From 8f92d4e6cd3fb0077b79da883d9cf44d98fb78b2 Mon Sep 17 00:00:00 2001 From: Cyrus Ho Date: Sat, 28 Feb 2026 01:59:55 +0800 Subject: [PATCH] test(sso): add tests for authorizationEndpoint hydration and discovery logic --- packages/sso/src/oidc.test.ts | 82 +++++++++++++++++++++++++ packages/sso/src/oidc/discovery.test.ts | 15 ++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/sso/src/oidc.test.ts b/packages/sso/src/oidc.test.ts index 84da13ff52..3f852e1c2b 100644 --- a/packages/sso/src/oidc.test.ts +++ b/packages/sso/src/oidc.test.ts @@ -254,6 +254,88 @@ describe("SSO", async () => { expect(callbackURL).toContain("/dashboard"); }); + it("should hydrate authorizationEndpoint via discovery when missing from stored config", async () => { + const { headers } = await signInWithTestUser(); + + // Register a provider with skipDiscovery, providing tokenEndpoint + + // jwksEndpoint but deliberately omitting authorizationEndpoint. + // This simulates a legacy provider stored without the authorization URL. + await auth.api.registerSSOProvider({ + body: { + issuer: server.issuer.url!, + domain: "no-auth-endpoint.com", + providerId: "no-auth-endpoint", + oidcConfig: { + clientId: "test", + clientSecret: "test", + skipDiscovery: true, + tokenEndpoint: `${server.issuer.url}/token`, + jwksEndpoint: `${server.issuer.url}/jwks`, + discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`, + mapping: { + id: "sub", + email: "email", + emailVerified: "email_verified", + name: "name", + image: "picture", + }, + }, + }, + headers, + }); + + // Use a unique identity so the callback doesn't collide with the + // "sso-user@localhost:8000.com" account already linked to "test" provider. + const originalUserinfoListeners = + server.service.listeners("beforeUserinfo"); + const originalTokenListeners = + server.service.listeners("beforeTokenSigning"); + server.service.removeAllListeners("beforeUserinfo"); + server.service.removeAllListeners("beforeTokenSigning"); + server.service.on("beforeUserinfo", (userInfoResponse: any) => { + userInfoResponse.body = { + email: "no-auth-endpoint-user@no-auth-endpoint.com", + name: "No Auth Endpoint User", + sub: "no-auth-endpoint-user", + email_verified: true, + }; + userInfoResponse.statusCode = 200; + }); + server.service.on("beforeTokenSigning", (token: any) => { + token.payload.email = "no-auth-endpoint-user@no-auth-endpoint.com"; + token.payload.email_verified = true; + token.payload.name = "No Auth Endpoint User"; + token.payload.sub = "no-auth-endpoint-user"; + }); + + try { + const signInHeaders = new Headers(); + const res = await authClient.signIn.sso({ + providerId: "no-auth-endpoint", + callbackURL: "/dashboard", + fetchOptions: { + throw: true, + onSuccess: cookieSetter(signInHeaders), + }, + }); + + // Discovery should have hydrated authorizationEndpoint — no error + expect(res.url).toContain("http://localhost:8080/authorize"); + + const { callbackURL } = await simulateOAuthFlow(res.url, signInHeaders); + expect(callbackURL).toContain("/dashboard"); + } finally { + server.service.removeAllListeners("beforeUserinfo"); + server.service.removeAllListeners("beforeTokenSigning"); + for (const listener of originalUserinfoListeners) { + server.service.on("beforeUserinfo", listener as any); + } + for (const listener of originalTokenListeners) { + server.service.on("beforeTokenSigning", listener as any); + } + } + }); + it("should normalize email to lowercase in OIDC authentication", async () => { const { headers } = await signInWithTestUser(); diff --git a/packages/sso/src/oidc/discovery.test.ts b/packages/sso/src/oidc/discovery.test.ts index 5ed24e67ff..a40690e269 100644 --- a/packages/sso/src/oidc/discovery.test.ts +++ b/packages/sso/src/oidc/discovery.test.ts @@ -629,13 +629,23 @@ describe("OIDC Discovery", () => { ).toBe(true); }); - it("should return false if both tokenEndpoint and jwksEndpoint are present", () => { + it("should return false if tokenEndpoint, jwksEndpoint and authorizationEndpoint are all present", () => { + expect( + needsRuntimeDiscovery({ + tokenEndpoint: "https://idp.example.com/oauth2/token", + jwksEndpoint: "https://idp.example.com/.well-known/jwks.json", + authorizationEndpoint: "https://idp.example.com/oauth2/authorize", + }), + ).toBe(false); + }); + + it("should return true if authorizationEndpoint is missing", () => { expect( needsRuntimeDiscovery({ tokenEndpoint: "https://idp.example.com/oauth2/token", jwksEndpoint: "https://idp.example.com/.well-known/jwks.json", }), - ).toBe(false); + ).toBe(true); }); }); @@ -1176,6 +1186,7 @@ describe("ensureRuntimeDiscovery", () => { it("returns config unchanged when discovery is not needed", async () => { const completeConfig = { ...baseConfig, + authorizationEndpoint: `${issuer}/oauth2/authorize`, tokenEndpoint: `${issuer}/oauth2/token`, jwksEndpoint: `${issuer}/.well-known/jwks.json`, };