fix(SSO): SAML認証が正常に動作しない問題を修正 (MisskeyIO#525)

This commit is contained in:
まっちゃとーにゅ 2024-03-16 09:01:03 +09:00 committed by GitHub
parent b33cc203ac
commit 142a906dec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 151 additions and 116 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 };

View File

@ -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();

View File

@ -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,
}); });
} }

View File

@ -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;

View File

@ -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: