diff --git a/packages/sso/src/index.ts b/packages/sso/src/index.ts index de83a0494a..ee751b4ee6 100644 --- a/packages/sso/src/index.ts +++ b/packages/sso/src/index.ts @@ -252,6 +252,7 @@ export const sso = (options?: SSOOptions) => { }, async (ctx) => { const provider = await ctx.context.adapter.findOne<{ + id: string; samlConfig: string; }>({ model: "ssoProvider", @@ -268,10 +269,29 @@ export const sso = (options?: SSOOptions) => { }); } - const parsedSamlConfig = JSON.parse(provider.samlConfig); - const sp = saml.ServiceProvider({ - metadata: parsedSamlConfig.spMetadata.metadata, - }); + const parsedSamlConfig: SAMLConfig = JSON.parse(provider.samlConfig); + const sp = parsedSamlConfig.spMetadata.metadata + ? saml.ServiceProvider({ + metadata: parsedSamlConfig.spMetadata.metadata, + }) + : saml.SPMetadata({ + entityID: + parsedSamlConfig.spMetadata?.entityID || + parsedSamlConfig.issuer, + assertionConsumerService: [ + { + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + Location: + parsedSamlConfig.callbackUrl || + `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.id}`, + }, + ], + wantMessageSigned: + parsedSamlConfig.wantAssertionsSigned || false, + nameIDFormat: parsedSamlConfig.identifierFormat + ? [parsedSamlConfig.identifierFormat] + : undefined, + }); return new Response(sp.getMetadata(), { headers: { "Content-Type": "application/xml", @@ -1004,7 +1024,7 @@ export const sso = (options?: SSOOptions) => { }); } if (provider.samlConfig) { - const parsedSamlConfig = + const parsedSamlConfig: SAMLConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : JSON.parse(provider.samlConfig as unknown as string); @@ -1014,11 +1034,11 @@ export const sso = (options?: SSOOptions) => { }); const idp = saml.IdentityProvider({ - metadata: parsedSamlConfig.idpMetadata.metadata, - entityID: parsedSamlConfig.idpMetadata.entityID, - encryptCert: parsedSamlConfig.idpMetadata.cert, + metadata: parsedSamlConfig.idpMetadata?.metadata, + entityID: parsedSamlConfig.idpMetadata?.entityID, + encryptCert: parsedSamlConfig.idpMetadata?.cert, singleSignOnService: - parsedSamlConfig.idpMetadata.singleSignOnService, + parsedSamlConfig.idpMetadata?.singleSignOnService, }); const loginRequest = sp.createLoginRequest( idp, @@ -1507,6 +1527,9 @@ export const sso = (options?: SSOOptions) => { encPrivateKey: spData?.encPrivateKey, encPrivateKeyPass: spData?.encPrivateKeyPass, wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false, + nameIDFormat: parsedSamlConfig.identifierFormat + ? [parsedSamlConfig.identifierFormat] + : undefined, }); let parsedResponse: FlowResult; @@ -1794,7 +1817,7 @@ export const sso = (options?: SSOOptions) => { Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", Location: parsedSamlConfig.callbackUrl || - `${ctx.context.baseURL}/sso/saml2/sp/acs`, + `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`, }, ], wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false, @@ -1803,6 +1826,9 @@ export const sso = (options?: SSOOptions) => { parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey, privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass, + nameIDFormat: parsedSamlConfig.identifierFormat + ? [parsedSamlConfig.identifierFormat] + : undefined, }); // Update where we construct the IdP diff --git a/packages/sso/src/saml.test.ts b/packages/sso/src/saml.test.ts index 74293cf35b..7fa4ef5c67 100644 --- a/packages/sso/src/saml.test.ts +++ b/packages/sso/src/saml.test.ts @@ -242,7 +242,7 @@ const certificate = ` yyoWAJDUHiAmvFA= -----END CERTIFICATE----- `; -const idpEncyptionKey = ` +const idpEncryptionKey = ` -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,860FDB9F3BE14699 @@ -274,7 +274,7 @@ const idpEncyptionKey = ` ISbutnQPUN5fsaIsgKDIV3T7n6519t6brobcW5bdigmf5ebFeZJ16/lYy6V77UM5 -----END RSA PRIVATE KEY----- `; -const spEncyptionKey = ` +const spEncryptionKey = ` -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,860FDB9F3BE14699 @@ -698,7 +698,7 @@ describe("SAML SSO", async () => { privateKey: idpPrivateKey, privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW", isAssertionEncrypted: true, - encPrivateKey: idpEncyptionKey, + encPrivateKey: idpEncryptionKey, encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN", }, spMetadata: { @@ -707,7 +707,7 @@ describe("SAML SSO", async () => { privateKey: spPrivateKey, privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px", isAssertionEncrypted: true, - encPrivateKey: spEncyptionKey, + encPrivateKey: spEncryptionKey, encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU", }, identifierFormat: @@ -754,7 +754,7 @@ describe("SAML SSO", async () => { privateKey: idpPrivateKey, privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW", isAssertionEncrypted: true, - encPrivateKey: idpEncyptionKey, + encPrivateKey: idpEncryptionKey, encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN", }, spMetadata: { @@ -763,7 +763,7 @@ describe("SAML SSO", async () => { privateKey: spPrivateKey, privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px", isAssertionEncrypted: true, - encPrivateKey: spEncyptionKey, + encPrivateKey: spEncryptionKey, encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU", }, identifierFormat: @@ -782,6 +782,69 @@ describe("SAML SSO", async () => { expect(spMetadataRes.status).toBe(200); expect(spMetadataResResValue).toBe(spMetadata); }); + it("Should fetch sp metadata", async () => { + const headers = await getAuthHeaders(); + await authClient.signIn.email(testUser, { + throw: true, + onSuccess: setCookieToHeader(headers), + }); + const issuer = "http://localhost:8081"; + const provider = await auth.api.registerSSOProvider({ + body: { + providerId: "saml-provider-1", + issuer: issuer, + domain: issuer, + samlConfig: { + entryPoint: mockIdP.metadataUrl, + cert: certificate, + callbackUrl: `${issuer}/api/sso/saml2/sp/acs`, + wantAssertionsSigned: false, + signatureAlgorithm: "sha256", + digestAlgorithm: "sha256", + idpMetadata: { + metadata: idpMetadata, + privateKey: idpPrivateKey, + privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW", + isAssertionEncrypted: true, + encPrivateKey: idpEncryptionKey, + encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN", + }, + spMetadata: { + binding: "post", + privateKey: spPrivateKey, + privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px", + isAssertionEncrypted: true, + encPrivateKey: spEncryptionKey, + encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU", + }, + identifierFormat: + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + }, + }, + headers, + }); + + const spMetadataRes = await auth.api.spMetadata({ + query: { + providerId: provider.providerId, + }, + }); + const spMetadataResResValue = await spMetadataRes.text(); + expect(spMetadataRes.status).toBe(200); + expect(spMetadataResResValue).toBeDefined(); + expect(spMetadataResResValue).toContain( + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + ); + expect(spMetadataResResValue).toContain( + "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + ); + expect(spMetadataResResValue).toContain( + ` { const headers = await getAuthHeaders(); const res = await authClient.signIn.email(testUser, { @@ -805,7 +868,7 @@ describe("SAML SSO", async () => { privateKey: idpPrivateKey, privateKeyPass: "q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW", isAssertionEncrypted: true, - encPrivateKey: idpEncyptionKey, + encPrivateKey: idpEncryptionKey, encPrivateKeyPass: "g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN", }, spMetadata: { @@ -814,7 +877,7 @@ describe("SAML SSO", async () => { privateKey: spPrivateKey, privateKeyPass: "VHOSp5RUiBcrsjrcAuXFwU1NKCkGA8px", isAssertionEncrypted: true, - encPrivateKey: spEncyptionKey, + encPrivateKey: spEncryptionKey, encPrivateKeyPass: "BXFNKpxrsjrCkGA8cAu5wUVHOSpci1RU", }, identifierFormat: