feat(sso): provide default service provider metadata (#4866)

This commit is contained in:
Dylan Vanmali
2025-09-24 04:26:39 +00:00
committed by GitHub
parent 864dec65af
commit e892a53b89
2 changed files with 107 additions and 18 deletions
+36 -10
View File
@@ -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
+71 -8
View File
@@ -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(
`<EntityDescriptor entityID="${issuer}"`,
);
expect(spMetadataResResValue).toContain(
`Location="${issuer}/api/sso/saml2/sp/acs"`,
);
});
it("should initiate SAML login and handle response", async () => {
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: