fix(SSO): SAML認証が正常に動作しない問題を修正 (MisskeyIO#525)
This commit is contained in:
parent
b33cc203ac
commit
142a906dec
|
@ -143,6 +143,7 @@
|
||||||
"nanoid": "5.0.6",
|
"nanoid": "5.0.6",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
|
"node-forge": "1.3.1",
|
||||||
"nodemailer": "6.9.12",
|
"nodemailer": "6.9.12",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
|
@ -213,6 +214,7 @@
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.11.27",
|
"@types/node": "20.11.27",
|
||||||
|
"@types/node-forge": "1.3.11",
|
||||||
"@types/nodemailer": "6.4.14",
|
"@types/nodemailer": "6.4.14",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.4",
|
||||||
"@types/oauth2orize": "1.11.4",
|
"@types/oauth2orize": "1.11.4",
|
||||||
|
|
|
@ -125,7 +125,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
name: ps.name ? ps.name : null,
|
name: ps.name ? ps.name : null,
|
||||||
type: ps.type,
|
type: ps.type,
|
||||||
issuer: ps.issuer,
|
issuer: ps.issuer,
|
||||||
audience: ps.audience?.filter(i => !!i),
|
audience: ps.audience?.filter(i => i.length > 0) ?? [],
|
||||||
acsUrl: ps.acsUrl,
|
acsUrl: ps.acsUrl,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
|
|
|
@ -48,6 +48,10 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
useCertificate: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
publicKey: {
|
publicKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -100,6 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
issuer: service.issuer,
|
issuer: service.issuer,
|
||||||
audience: service.audience,
|
audience: service.audience,
|
||||||
acsUrl: service.acsUrl,
|
acsUrl: service.acsUrl,
|
||||||
|
useCertificate: service.privateKey != null,
|
||||||
publicKey: service.publicKey,
|
publicKey: service.publicKey,
|
||||||
signatureAlgorithm: service.signatureAlgorithm,
|
signatureAlgorithm: service.signatureAlgorithm,
|
||||||
cipherAlgorithm: service.cipherAlgorithm,
|
cipherAlgorithm: service.cipherAlgorithm,
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
await this.singleSignOnServiceProviderRepository.update(service.id, {
|
await this.singleSignOnServiceProviderRepository.update(service.id, {
|
||||||
name: ps.name !== '' ? ps.name : null,
|
name: ps.name !== '' ? ps.name : null,
|
||||||
issuer: ps.issuer,
|
issuer: ps.issuer,
|
||||||
audience: ps.audience?.filter(i => !!i),
|
audience: ps.audience?.filter(i => i.length > 0),
|
||||||
acsUrl: ps.acsUrl,
|
acsUrl: ps.acsUrl,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import forge from 'node-forge';
|
||||||
import * as jose from 'jose';
|
import * as jose from 'jose';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import * as saml from 'samlify';
|
import * as saml from 'samlify';
|
||||||
|
@ -55,17 +56,33 @@ export class SAMLIdentifyProviderService {
|
||||||
public async createIdPMetadataXml(
|
public async createIdPMetadataXml(
|
||||||
provider: MiSingleSignOnServiceProvider,
|
provider: MiSingleSignOnServiceProvider,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const today = new Date();
|
const nowTime = new Date();
|
||||||
const publicKey = await jose
|
const tenYearsLaterTime = new Date(nowTime.getTime());
|
||||||
.importJWK(JSON.parse(provider.publicKey))
|
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
||||||
|
const tenYearsLater = tenYearsLaterTime.toISOString();
|
||||||
|
|
||||||
|
const cert = forge.pki.createCertificate();
|
||||||
|
cert.serialNumber = '01';
|
||||||
|
cert.validity.notBefore = provider.createdAt;
|
||||||
|
cert.validity.notAfter = tenYearsLaterTime;
|
||||||
|
const attrs = [{ name: 'commonName', value: this.config.hostname }];
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
cert.publicKey = await jose.importJWK(JSON.parse(provider.publicKey))
|
||||||
.then(k => jose.exportSPKI(k as jose.KeyLike))
|
.then(k => jose.exportSPKI(k as jose.KeyLike))
|
||||||
.then(k => k.replace(/-----(?:BEGIN|END) PUBLIC KEY-----|\s/g, ''));
|
.then(k => forge.pki.publicKeyFromPem(k));
|
||||||
|
cert.sign(
|
||||||
|
await jose.importJWK(JSON.parse(provider.privateKey ?? '{}'))
|
||||||
|
.then(k => jose.exportPKCS8(k as jose.KeyLike))
|
||||||
|
.then(k => forge.pki.privateKeyFromPem(k)),
|
||||||
|
forge.md.sha256.create(),
|
||||||
|
);
|
||||||
|
|
||||||
const nodes = {
|
const nodes = {
|
||||||
'md:EntityDescriptor': {
|
'md:EntityDescriptor': {
|
||||||
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||||
'@entityID': provider.issuer,
|
'@entityID': provider.issuer,
|
||||||
'@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(),
|
'@validUntil': tenYearsLater,
|
||||||
'md:IDPSSODescriptor': {
|
'md:IDPSSODescriptor': {
|
||||||
'@WantAuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
'@WantAuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
||||||
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||||
|
@ -75,7 +92,7 @@ export class SAMLIdentifyProviderService {
|
||||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||||
'ds:X509Data': {
|
'ds:X509Data': {
|
||||||
'ds:X509Certificate': {
|
'ds:X509Certificate': {
|
||||||
'#text': publicKey,
|
'#text': forge.pki.certificateToPem(cert).replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, ''),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -88,10 +105,6 @@ export class SAMLIdentifyProviderService {
|
||||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||||
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
|
||||||
'@Location': `${this.config.url}/sso/saml/${provider.id}`,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -105,11 +118,28 @@ export class SAMLIdentifyProviderService {
|
||||||
public async createSPMetadataXml(
|
public async createSPMetadataXml(
|
||||||
provider: MiSingleSignOnServiceProvider,
|
provider: MiSingleSignOnServiceProvider,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const today = new Date();
|
const nowTime = new Date();
|
||||||
const publicKey = await jose
|
const tenYearsLaterTime = new Date(nowTime.getTime());
|
||||||
.importJWK(JSON.parse(provider.publicKey))
|
tenYearsLaterTime.setFullYear(tenYearsLaterTime.getFullYear() + 10);
|
||||||
|
const tenYearsLater = tenYearsLaterTime.toISOString();
|
||||||
|
|
||||||
|
const cert = forge.pki.createCertificate();
|
||||||
|
cert.serialNumber = '01';
|
||||||
|
cert.validity.notBefore = provider.createdAt;
|
||||||
|
cert.validity.notAfter = tenYearsLaterTime;
|
||||||
|
const attrs = [{ name: 'commonName', value: this.config.hostname }];
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
cert.publicKey = await jose.importJWK(JSON.parse(provider.publicKey))
|
||||||
.then(k => jose.exportSPKI(k as jose.KeyLike))
|
.then(k => jose.exportSPKI(k as jose.KeyLike))
|
||||||
.then(k => k.replace(/-----(?:BEGIN|END) PUBLIC KEY-----|\s/g, ''));
|
.then(k => forge.pki.publicKeyFromPem(k));
|
||||||
|
cert.sign(
|
||||||
|
await jose.importJWK(JSON.parse(provider.privateKey ?? '{}'))
|
||||||
|
.then(k => jose.exportPKCS8(k as jose.KeyLike))
|
||||||
|
.then(k => forge.pki.privateKeyFromPem(k)),
|
||||||
|
forge.md.sha256.create(),
|
||||||
|
);
|
||||||
|
const x509 = forge.pki.certificateToPem(cert).replace(/-----(?:BEGIN|END) CERTIFICATE-----|\s/g, '');
|
||||||
|
|
||||||
const keyDescriptor: unknown[] = [
|
const keyDescriptor: unknown[] = [
|
||||||
{
|
{
|
||||||
|
@ -118,7 +148,7 @@ export class SAMLIdentifyProviderService {
|
||||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||||
'ds:X509Data': {
|
'ds:X509Data': {
|
||||||
'ds:X509Certificate': {
|
'ds:X509Certificate': {
|
||||||
'#text': publicKey,
|
'#text': x509,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -132,13 +162,13 @@ export class SAMLIdentifyProviderService {
|
||||||
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
'@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#',
|
||||||
'ds:X509Data': {
|
'ds:X509Data': {
|
||||||
'ds:X509Certificate': {
|
'ds:X509Certificate': {
|
||||||
'#text': publicKey,
|
'#text': x509,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'md:EncryptionMethod': {
|
'md:EncryptionMethod': [
|
||||||
'@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
|
{ '@Algorithm': `http://www.w3.org/2001/04/xmlenc#${provider.cipherAlgorithm}` },
|
||||||
},
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +176,7 @@ export class SAMLIdentifyProviderService {
|
||||||
'md:EntityDescriptor': {
|
'md:EntityDescriptor': {
|
||||||
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
'@xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||||
'@entityID': provider.issuer,
|
'@entityID': provider.issuer,
|
||||||
'@validUntil': new Date(today.setFullYear(today.getFullYear() + 10)).toISOString(),
|
'@validUntil': tenYearsLater,
|
||||||
'md:SPSSODescriptor': {
|
'md:SPSSODescriptor': {
|
||||||
'@AuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
'@AuthnRequestsSigned': provider.wantAuthnRequestsSigned,
|
||||||
'@WantAssertionsSigned': provider.wantAssertionsSigned,
|
'@WantAssertionsSigned': provider.wantAssertionsSigned,
|
||||||
|
@ -155,11 +185,13 @@ export class SAMLIdentifyProviderService {
|
||||||
'md:NameIDFormat': {
|
'md:NameIDFormat': {
|
||||||
'#text': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
'#text': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
||||||
},
|
},
|
||||||
'md:AssertionConsumerService': {
|
'md:AssertionConsumerService': [
|
||||||
'@index': 1,
|
{
|
||||||
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
|
'@index': 1,
|
||||||
'@Location': provider.acsUrl,
|
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||||
},
|
'@Location': provider.acsUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -203,7 +235,7 @@ export class SAMLIdentifyProviderService {
|
||||||
Body?: { SAMLRequest?: string; RelayState?: string };
|
Body?: { SAMLRequest?: string; RelayState?: string };
|
||||||
}>('/:serviceId', async (request, reply) => {
|
}>('/:serviceId', async (request, reply) => {
|
||||||
const serviceId = request.params.serviceId;
|
const serviceId = request.params.serviceId;
|
||||||
const binding = request.query?.SAMLRequest ? 'redirect' : 'post';
|
const binding = 'redirect'; // 今はリダイレクトのみ対応 request.query?.SAMLRequest ? 'redirect' : 'post';
|
||||||
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
|
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
|
||||||
const relayState = request.query?.RelayState ?? request.body?.RelayState;
|
const relayState = request.query?.RelayState ?? request.body?.RelayState;
|
||||||
|
|
||||||
|
@ -236,38 +268,63 @@ export class SAMLIdentifyProviderService {
|
||||||
metadata: await this.createIdPMetadataXml(ssoServiceProvider),
|
metadata: await this.createIdPMetadataXml(ssoServiceProvider),
|
||||||
privateKey: await jose
|
privateKey: await jose
|
||||||
.importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}'))
|
.importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}'))
|
||||||
.then(k => jose.exportPKCS8(k as jose.KeyLike))
|
.then(k => jose.exportPKCS8(k as jose.KeyLike)),
|
||||||
.then(k => k.replace(/-----(?:BEGIN|END) PRIVATE KEY-----|\s/g, '')),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sp = saml.ServiceProvider({
|
const sp = saml.ServiceProvider({
|
||||||
metadata: await this.createSPMetadataXml(ssoServiceProvider),
|
metadata: await this.createSPMetadataXml(ssoServiceProvider),
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = await idp.parseLoginRequest(sp, binding, { query: request.query, body: request.body });
|
try {
|
||||||
this.#logger.info('Parsed SAML request', { saml: parsed });
|
const parsed = await idp.parseLoginRequest(sp, binding, { query: request.query, body: request.body });
|
||||||
|
this.#logger.info('Parsed SAML request', { saml: parsed });
|
||||||
|
|
||||||
const transactionId = randomUUID();
|
const transactionId = randomUUID();
|
||||||
await this.redisClient.set(
|
await this.redisClient.set(
|
||||||
`sso:saml:transaction:${transactionId}`,
|
`sso:saml:transaction:${transactionId}`,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
serviceId: serviceId,
|
serviceId: serviceId,
|
||||||
binding: binding,
|
binding: binding,
|
||||||
flowResult: parsed,
|
flowResult: parsed,
|
||||||
relayState: relayState,
|
relayState: relayState,
|
||||||
}),
|
}),
|
||||||
'EX',
|
'EX',
|
||||||
60 * 5,
|
60 * 5,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#logger.info(`Rendering authorization page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`);
|
this.#logger.info(`Rendering authorization page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`);
|
||||||
|
|
||||||
reply.header('Cache-Control', 'no-store');
|
reply.header('Cache-Control', 'no-store');
|
||||||
return await reply.view('sso', {
|
return await reply.view('sso', {
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
|
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
|
||||||
kind: 'saml',
|
kind: 'saml',
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.#logger.error('Failed to parse SAML request', { error: err });
|
||||||
|
const traceableError = err as Error & {code?: string};
|
||||||
|
|
||||||
|
if (traceableError.code) {
|
||||||
|
reply.status(500).send({
|
||||||
|
error: {
|
||||||
|
message: traceableError.message,
|
||||||
|
code: traceableError.code,
|
||||||
|
id: 'a8aa8e69-a4c3-4148-8efe-fe3e8cb89684',
|
||||||
|
kind: 'client',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.status(400).send({
|
||||||
|
error: {
|
||||||
|
message: 'Invalid SAML Request',
|
||||||
|
code: 'INVALID_SAML_REQUEST',
|
||||||
|
id: '874b9cc2-71cb-4000-95c7-449391ee9861',
|
||||||
|
kind: 'client',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { serviceId: string } }>(
|
fastify.get<{ Params: { serviceId: string } }>(
|
||||||
|
@ -371,8 +428,7 @@ export class SAMLIdentifyProviderService {
|
||||||
metadata: await this.createIdPMetadataXml(ssoServiceProvider),
|
metadata: await this.createIdPMetadataXml(ssoServiceProvider),
|
||||||
privateKey: await jose
|
privateKey: await jose
|
||||||
.importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}'))
|
.importJWK(JSON.parse(ssoServiceProvider.privateKey ?? '{}'))
|
||||||
.then(k => jose.exportPKCS8(k as jose.KeyLike))
|
.then(k => jose.exportPKCS8(k as jose.KeyLike)),
|
||||||
.then(k => k.replace(/-----(?:BEGIN|END) PRIVATE KEY-----|\s/g, '')),
|
|
||||||
loginResponseTemplate: { context: 'ignored' },
|
loginResponseTemplate: { context: 'ignored' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -380,7 +436,7 @@ export class SAMLIdentifyProviderService {
|
||||||
metadata: await this.createSPMetadataXml(ssoServiceProvider),
|
metadata: await this.createSPMetadataXml(ssoServiceProvider),
|
||||||
});
|
});
|
||||||
|
|
||||||
const samlResponse = await idp.createLoginResponse(
|
const loginResponse = await idp.createLoginResponse(
|
||||||
sp,
|
sp,
|
||||||
flowResult,
|
flowResult,
|
||||||
binding,
|
binding,
|
||||||
|
@ -422,8 +478,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
'saml:Subject': {
|
'saml:Subject': {
|
||||||
'saml:NameID': {
|
'saml:NameID': {
|
||||||
'@Format':
|
'@Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
||||||
'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
|
|
||||||
'#text': user.id,
|
'#text': user.id,
|
||||||
},
|
},
|
||||||
'saml:SubjectConfirmation': {
|
'saml:SubjectConfirmation': {
|
||||||
|
@ -453,8 +508,7 @@ export class SAMLIdentifyProviderService {
|
||||||
'@SessionNotOnOrAfter': fiveMinutesLater,
|
'@SessionNotOnOrAfter': fiveMinutesLater,
|
||||||
'saml:AuthnContext': {
|
'saml:AuthnContext': {
|
||||||
'saml:AuthnContextClassRef': {
|
'saml:AuthnContextClassRef': {
|
||||||
'#text':
|
'#text': 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
|
||||||
'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -462,8 +516,7 @@ export class SAMLIdentifyProviderService {
|
||||||
'saml:Attribute': [
|
'saml:Attribute': [
|
||||||
{
|
{
|
||||||
'@Name': 'identityprovider',
|
'@Name': 'identityprovider',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': this.config.url,
|
'#text': this.config.url,
|
||||||
|
@ -471,8 +524,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'uid',
|
'@Name': 'uid',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': user.id,
|
'#text': user.id,
|
||||||
|
@ -480,8 +532,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'displayname',
|
'@Name': 'displayname',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': user.name,
|
'#text': user.name,
|
||||||
|
@ -489,8 +540,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'name',
|
'@Name': 'name',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': user.username,
|
'#text': user.username,
|
||||||
|
@ -498,8 +548,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'preferred_username',
|
'@Name': 'preferred_username',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': user.username,
|
'#text': user.username,
|
||||||
|
@ -507,8 +556,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'profile',
|
'@Name': 'profile',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': `${this.config.url}/@${user.username}`,
|
'#text': `${this.config.url}/@${user.username}`,
|
||||||
|
@ -516,8 +564,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'picture',
|
'@Name': 'picture',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': user.avatarUrl,
|
'#text': user.avatarUrl,
|
||||||
|
@ -525,8 +572,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'mail',
|
'@Name': 'mail',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': profile.email,
|
'#text': profile.email,
|
||||||
|
@ -534,8 +580,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'email',
|
'@Name': 'email',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:string',
|
'@xsi:type': 'xs:string',
|
||||||
'#text': profile.email,
|
'#text': profile.email,
|
||||||
|
@ -543,8 +588,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'email_verified',
|
'@Name': 'email_verified',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:boolean',
|
'@xsi:type': 'xs:boolean',
|
||||||
'#text': profile.emailVerified,
|
'#text': profile.emailVerified,
|
||||||
|
@ -552,8 +596,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'mfa_enabled',
|
'@Name': 'mfa_enabled',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:boolean',
|
'@xsi:type': 'xs:boolean',
|
||||||
'#text': profile.twoFactorEnabled,
|
'#text': profile.twoFactorEnabled,
|
||||||
|
@ -561,8 +604,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'updated_at',
|
'@Name': 'updated_at',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:integer',
|
'@xsi:type': 'xs:integer',
|
||||||
'#text': Math.floor((user.updatedAt?.getTime() ?? user.createdAt.getTime()) / 1000),
|
'#text': Math.floor((user.updatedAt?.getTime() ?? user.createdAt.getTime()) / 1000),
|
||||||
|
@ -570,8 +612,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'admin',
|
'@Name': 'admin',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:boolean',
|
'@xsi:type': 'xs:boolean',
|
||||||
'#text': isAdministrator,
|
'#text': isAdministrator,
|
||||||
|
@ -579,8 +620,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'moderator',
|
'@Name': 'moderator',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': {
|
'saml:AttributeValue': {
|
||||||
'@xsi:type': 'xs:boolean',
|
'@xsi:type': 'xs:boolean',
|
||||||
'#text': isModerator,
|
'#text': isModerator,
|
||||||
|
@ -588,8 +628,7 @@ export class SAMLIdentifyProviderService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@Name': 'roles',
|
'@Name': 'roles',
|
||||||
'@NameFormat':
|
'@NameFormat': 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
||||||
'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
|
|
||||||
'saml:AttributeValue': [
|
'saml:AttributeValue': [
|
||||||
...roles
|
...roles
|
||||||
.filter((r) => r.isPublic)
|
.filter((r) => r.isPublic)
|
||||||
|
@ -616,7 +655,7 @@ export class SAMLIdentifyProviderService {
|
||||||
relayState,
|
relayState,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#logger.info(`Rendering SAML response page for "${ssoServiceProvider.name ?? ssoServiceProvider.issuer}"`, {
|
this.#logger.info(`Redirecting to "${ssoServiceProvider.acsUrl}"`, {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
ssoServiceProvider: ssoServiceProvider.id,
|
ssoServiceProvider: ssoServiceProvider.id,
|
||||||
acsUrl: ssoServiceProvider.acsUrl,
|
acsUrl: ssoServiceProvider.acsUrl,
|
||||||
|
@ -624,11 +663,8 @@ export class SAMLIdentifyProviderService {
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.header('Cache-Control', 'no-store');
|
reply.header('Cache-Control', 'no-store');
|
||||||
return await reply.view('sso-saml-post', {
|
reply.redirect(loginResponse.context);
|
||||||
acsUrl: ssoServiceProvider.acsUrl,
|
return;
|
||||||
samlResponse: samlResponse,
|
|
||||||
relyState: relayState ?? null,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.#logger.error('Failed to create SAML response', { error: err });
|
this.#logger.error('Failed to create SAML response', { error: err });
|
||||||
const traceableError = err as Error & { code?: string };
|
const traceableError = err as Error & { code?: string };
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
html
|
|
||||||
body
|
|
||||||
noscript: p
|
|
||||||
| JavaScriptを有効にしてください
|
|
||||||
br
|
|
||||||
| Please turn on your JavaScript
|
|
||||||
|
|
||||||
p
|
|
||||||
| Redirecting...
|
|
||||||
|
|
||||||
form(id='sso', method='post', action=action autocomplete='off')
|
|
||||||
input(type='hidden', name='SAMLResponse', value=samlResponse)
|
|
||||||
|
|
||||||
if relayState !== null
|
|
||||||
input(type='hidden', name='RelayState', value=relayState)
|
|
||||||
|
|
||||||
button(type='submit')
|
|
||||||
| click here if you are not redirected.
|
|
||||||
|
|
||||||
script.
|
|
||||||
document.forms[0].submit();
|
|
|
@ -427,12 +427,12 @@ function ssoServiceAddNew() {
|
||||||
issuer: '',
|
issuer: '',
|
||||||
audience: '',
|
audience: '',
|
||||||
acsUrl: '',
|
acsUrl: '',
|
||||||
|
useCertificate: false,
|
||||||
publicKey: '',
|
publicKey: '',
|
||||||
signatureAlgorithm: 'HS256',
|
signatureAlgorithm: 'HS256',
|
||||||
cipherAlgorithm: '',
|
cipherAlgorithm: '',
|
||||||
wantAuthnRequestsSigned: false,
|
wantAuthnRequestsSigned: false,
|
||||||
wantAssertionsSigned: true,
|
wantAssertionsSigned: true,
|
||||||
useCertificate: false,
|
|
||||||
regenerateCertificate: false,
|
regenerateCertificate: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -10488,6 +10488,7 @@ export type operations = {
|
||||||
issuer: string;
|
issuer: string;
|
||||||
audience: string[];
|
audience: string[];
|
||||||
acsUrl: string;
|
acsUrl: string;
|
||||||
|
useCertificate: boolean;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
signatureAlgorithm: string;
|
signatureAlgorithm: string;
|
||||||
cipherAlgorithm?: string | null;
|
cipherAlgorithm?: string | null;
|
||||||
|
|
|
@ -295,6 +295,9 @@ importers:
|
||||||
node-fetch:
|
node-fetch:
|
||||||
specifier: 3.3.2
|
specifier: 3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
|
node-forge:
|
||||||
|
specifier: 1.3.1
|
||||||
|
version: 1.3.1
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: 6.9.12
|
specifier: 6.9.12
|
||||||
version: 6.9.12
|
version: 6.9.12
|
||||||
|
@ -588,6 +591,9 @@ importers:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.11.27
|
specifier: 20.11.27
|
||||||
version: 20.11.27
|
version: 20.11.27
|
||||||
|
'@types/node-forge':
|
||||||
|
specifier: 1.3.11
|
||||||
|
version: 1.3.11
|
||||||
'@types/nodemailer':
|
'@types/nodemailer':
|
||||||
specifier: 6.4.14
|
specifier: 6.4.14
|
||||||
version: 6.4.14
|
version: 6.4.14
|
||||||
|
@ -7509,6 +7515,12 @@ packages:
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/node-forge@1.3.11:
|
||||||
|
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.11.27
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node@18.19.24:
|
/@types/node@18.19.24:
|
||||||
resolution: {integrity: sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==}
|
resolution: {integrity: sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in New Issue