mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-26 02:16:23 -05:00
feat(sso): provide default service provider metadata (#4866)
This commit is contained in:
+36
-10
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user