// ---------------------------------------------------------------------------// Fidelity suite — locks in every edge case the prior hand-rolled// Google OAuth integrations handled, so future "simplifications" of the// shared helpers fail loudly instead of silently breaking refresh / parsing /// provider-specific quirks.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Effect, Exit, Ref } from "effect";import { HttpServerResponse } from "effect/unstable/http"; import { OAUTH2_DEFAULT_TIMEOUT_MS, OAUTH2_REFRESH_SKEW_MS, OAuth2Error, buildAuthorizationUrl, providerAuthorizeExtras, createPkceCodeChallenge, createPkceCodeVerifier, exchangeAuthorizationCode, exchangeClientCredentials, idTokenIdentityLabel, refreshAccessToken, shouldRefreshToken,} from "./oauth-helpers";import { serveTestHttpApp } from "./testing"; interface TokenCall { readonly method: string; readonly url: string; readonly headers: Readonly<Record<string, string>>; readonly body: URLSearchParams;} type TokenHandler = (call: TokenCall) => HttpServerResponse.HttpServerResponse; const json = (status: number, body: unknown): HttpServerResponse.HttpServerResponse => HttpServerResponse.jsonUnsafe(body, { status }); const serveTokenEndpoint = (handler: TokenHandler) => Effect.gen(function* () { const calls = yield* Ref.make<readonly TokenCall[]>([]); const server = yield* serveTestHttpApp((request) => Effect.gen(function* () { const bodyText = yield* request.text; const call = { method: request.method, url: request.url ?? "/", headers: request.headers, body: new URLSearchParams(bodyText), }; yield* Ref.update(calls, (all) => [...all, call]); return handler(call); }).pipe( Effect.catch(() => Effect.succeed(HttpServerResponse.text("token fixture failed", { status: 500 })), ), ), ); return { tokenUrl: server.url("/token"), calls: Ref.get(calls), } as const; }); const withTokenEndpoint = <A, E>( handler: TokenHandler, use: (fixture: { readonly tokenUrl: string; readonly calls: Effect.Effect<readonly TokenCall[]>; }) => Effect.Effect<A, E>,) => Effect.scoped( Effect.gen(function* () { const fixture = yield* serveTokenEndpoint(handler); return yield* use(fixture); }), ); const validCodeBody = { access_token: "tok", token_type: "Bearer", refresh_token: "rtok", expires_in: 3600, scope: "read",}; const validRefreshBody = { access_token: "tok2", token_type: "Bearer", expires_in: 3600 }; const jwtPart = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url"); const unsignedJwt = (claims: Record<string, unknown>, alg = "RS256"): string => `${jwtPart({ alg, typ: "JWT" })}.${jwtPart(claims)}.sig`; const tokenResponse = (body: unknown): TokenHandler => () => json(200, body); const tokenResponseFetch = (body: unknown): typeof globalThis.fetch => async () => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); // ---------------------------------------------------------------------------// PKCE// --------------------------------------------------------------------------- describe("PKCE", () => { it("createPkceCodeVerifier returns a base64url string in the RFC 7636 length range", () => { for (let i = 0; i < 25; i++) { const verifier = createPkceCodeVerifier(); expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/); expect(verifier.length).toBeGreaterThanOrEqual(43); expect(verifier.length).toBeLessThanOrEqual(128); } }); it("createPkceCodeChallenge matches the RFC 7636 Appendix A test vector", async () => { const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; const expected = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; expect(await createPkceCodeChallenge(verifier)).toBe(expected); }); it("createPkceCodeVerifier produces unique values", () => { const seen = new Set<string>(); for (let i = 0; i < 50; i++) seen.add(createPkceCodeVerifier()); expect(seen.size).toBe(50); });}); // ---------------------------------------------------------------------------// buildAuthorizationUrl// --------------------------------------------------------------------------- describe("providerAuthorizeExtras (Google offline/consent quirk)", () => { it("adds access_type=offline + prompt=consent for the Google authorize host", () => { expect(providerAuthorizeExtras("https://accounts.google.com/o/oauth2/v2/auth")).toEqual({ access_type: "offline", prompt: "consent", }); }); it("adds nothing for non-Google hosts or an unparseable URL (token host ≠ authorize host)", () => { expect(providerAuthorizeExtras("https://accounts.spotify.com/authorize")).toEqual({}); expect(providerAuthorizeExtras("https://oauth2.googleapis.com/token")).toEqual({}); expect(providerAuthorizeExtras("not a url")).toEqual({}); });}); describe("buildAuthorizationUrl", () => { const baseInput = { authorizationUrl: "https://example.com/authorize", clientId: "client-123", redirectUrl: "https://app.example.com/callback", scopes: ["read", "write"] as const, state: "state-abc", codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", }; it("emits all RFC 6749 + PKCE params", () => { const url = new URL(buildAuthorizationUrl(baseInput)); expect(url.origin + url.pathname).toBe("https://example.com/authorize"); expect(url.searchParams.get("client_id")).toBe("client-123"); expect(url.searchParams.get("redirect_uri")).toBe("https://app.example.com/callback"); expect(url.searchParams.get("response_type")).toBe("code"); expect(url.searchParams.get("scope")).toBe("read write"); expect(url.searchParams.get("state")).toBe("state-abc"); expect(url.searchParams.get("code_challenge_method")).toBe("S256"); expect(url.searchParams.get("code_challenge")).toBe( "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", ); }); it("supports a custom scope separator (e.g. comma for legacy providers)", () => { const url = new URL(buildAuthorizationUrl({ ...baseInput, scopeSeparator: "," })); expect(url.searchParams.get("scope")).toBe("read,write"); }); it("omits scope when no scopes are requested", () => { const url = new URL(buildAuthorizationUrl({ ...baseInput, scopes: [] })); expect(url.searchParams.has("scope")).toBe(false); }); it("merges provider extra params without dropping them", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, extraParams: { access_type: "offline", prompt: "consent", }, }), ); expect(url.searchParams.get("access_type")).toBe("offline"); expect(url.searchParams.get("prompt")).toBe("consent"); expect(url.searchParams.has("include_granted_scopes")).toBe(false); expect(url.searchParams.get("code_challenge_method")).toBe("S256"); }); it("preserves pre-existing query params on the authorization URL", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, authorizationUrl: "https://example.com/auth?tenant=acme", }), ); expect(url.searchParams.get("tenant")).toBe("acme"); expect(url.searchParams.get("client_id")).toBe("client-123"); }); it("includes RFC 8707 resource indicator when provided", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, resource: "https://api.example.com/v1/mcp", }), ); expect(url.searchParams.get("resource")).toBe("https://api.example.com/v1/mcp"); }); it("omits resource parameter when not provided", () => { const url = new URL(buildAuthorizationUrl(baseInput)); expect(url.searchParams.has("resource")).toBe(false); }); it("rejects unsupported authorization URL schemes", () => { expect(() => buildAuthorizationUrl({ ...baseInput, authorizationUrl: "javascript:alert(1)", }), ).toThrow(/Authorization URL must use https: or loopback http:/); expect(() => buildAuthorizationUrl({ ...baseInput, authorizationUrl: "http://example.com/authorize", }), ).toThrow(/Authorization URL must use https: or loopback http:/); }); it("allows HTTP authorization URLs when the caller opts into local HTTP", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, authorizationUrl: "http://example.com/authorize", endpointUrlPolicy: { allowHttp: true }, }), ); expect(url.origin + url.pathname).toBe("http://example.com/authorize"); });}); describe("exchangeAuthorizationCode", () => { it.effect("posts form-urlencoded body with grant_type=authorization_code and PKCE verifier", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", clientSecret: "csecret", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); const call = (yield* calls)[0]!; expect(call.method).toBe("POST"); expect(call.headers["content-type"]).toMatch(/^application\/x-www-form-urlencoded/); expect(call.headers["accept"]).toContain("application/json"); expect(call.body.get("grant_type")).toBe("authorization_code"); expect(call.body.get("client_id")).toBe("cid"); expect(call.body.get("client_secret")).toBe("csecret"); expect(call.body.get("redirect_uri")).toBe("https://app.example.com/cb"); expect(call.body.get("code_verifier")).toBe("verifier"); expect(call.body.get("code")).toBe("abc"); }), ), ); it.effect("rejects unsupported token URL schemes before exchange", () => Effect.gen(function* () { const exit = yield* Effect.exit( exchangeAuthorizationCode({ tokenUrl: "http://example.com/token", clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; expect(JSON.stringify(exit.cause)).toContain("Token URL must use https: or loopback http:"); }), ); it.effect("omits client_secret when none is provided (public clients with PKCE)", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); const body = (yield* calls)[0]!.body; expect(body.get("client_id")).toBe("cid"); expect(body.has("client_secret")).toBe(false); }), ), ); it.effect("includes RFC 8707 resource parameter on the token request when provided", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", resource: "https://api.example.com/v1/mcp", }); expect((yield* calls)[0]!.body.get("resource")).toBe("https://api.example.com/v1/mcp"); }), ), ); it.effect("omits resource parameter when not provided", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect((yield* calls)[0]!.body.has("resource")).toBe(false); }), ), ); it.effect("strips id_tokens whose iss does not match AS metadata", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ iss: "https://us.posthog.com", aud: "cid", sub: "user-1", exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), }), }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, issuerUrl: new URL(tokenUrl).origin, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); expect(result.refresh_token).toBe("rtok"); expect(result.idTokenIdentityLabel).toBe("user-1"); }), ), ); it.effect("extracts id_token email as the identity label", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ email: "alice@example.com", preferred_username: "alice", sub: "user-1", }), }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.idTokenIdentityLabel).toBe("alice@example.com"); }), ), ); it.effect("falls back from id_token email to preferred_username then sub", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ preferred_username: "alice", sub: "user-1", }), }), ({ tokenUrl }) => Effect.gen(function* () { const preferred = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(preferred.idTokenIdentityLabel).toBe("alice"); }), ), ); it("falls back to sub and ignores malformed id_tokens", () => { expect(idTokenIdentityLabel(unsignedJwt({ sub: "user-1" }))).toBe("user-1"); expect(idTokenIdentityLabel("not-a-jwt")).toBeUndefined(); expect(idTokenIdentityLabel(undefined)).toBeUndefined(); }); it.effect("ignores malformed id_tokens without failing the exchange", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: "not-a-jwt", }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); expect(result.idTokenIdentityLabel).toBeUndefined(); }), ), ); it.effect("strips id_tokens whose aud does not match the client_id", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ iss: "http://127.0.0.1", aud: "another-client", sub: "user-1", exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), }), }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, issuerUrl: new URL(tokenUrl).origin, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); }), ), ); it.effect("happy path: token endpoint with no id_token still parses normally", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); expect(result.refresh_token).toBe("rtok"); expect(result.expires_in).toBe(3600); expect(result.idTokenIdentityLabel).toBeUndefined(); }), ), ); it.effect("uses nested granted scopes for Slack-style user token responses", () => withTokenEndpoint( tokenResponse({Lines 1–500 of 1169. Load more
// --------------------------------------------------------------------------- // Fidelity suite — locks in every edge case the prior hand-rolled // Google OAuth integrations handled, so future "simplifications" of the // shared helpers fail loudly instead of silently breaking refresh / parsing / // provider-specific quirks. // --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest"; import { Effect, Exit, Ref } from "effect"; import { HttpServerResponse } from "effect/unstable/http"; import { OAUTH2_DEFAULT_TIMEOUT_MS, OAUTH2_REFRESH_SKEW_MS, OAuth2Error, buildAuthorizationUrl, providerAuthorizeExtras, createPkceCodeChallenge, createPkceCodeVerifier, exchangeAuthorizationCode, exchangeClientCredentials, idTokenIdentityLabel, refreshAccessToken, shouldRefreshToken, } from "./oauth-helpers"; import { serveTestHttpApp } from "./testing"; interface TokenCall { readonly method: string; readonly url: string; readonly headers: Readonly<Record<string, string>>; readonly body: URLSearchParams; } type TokenHandler = (call: TokenCall) => HttpServerResponse.HttpServerResponse; const json = (status: number, body: unknown): HttpServerResponse.HttpServerResponse => HttpServerResponse.jsonUnsafe(body, { status }); const serveTokenEndpoint = (handler: TokenHandler) => Effect.gen(function* () { const calls = yield* Ref.make<readonly TokenCall[]>([]); const server = yield* serveTestHttpApp((request) => Effect.gen(function* () { const bodyText = yield* request.text; const call = { method: request.method, url: request.url ?? "/", headers: request.headers, body: new URLSearchParams(bodyText), }; yield* Ref.update(calls, (all) => [...all, call]); return handler(call); }).pipe( Effect.catch(() => Effect.succeed(HttpServerResponse.text("token fixture failed", { status: 500 })), ), ), ); return { tokenUrl: server.url("/token"), calls: Ref.get(calls), } as const; }); const withTokenEndpoint = <A, E>( handler: TokenHandler, use: (fixture: { readonly tokenUrl: string; readonly calls: Effect.Effect<readonly TokenCall[]>; }) => Effect.Effect<A, E>, ) => Effect.scoped( Effect.gen(function* () { const fixture = yield* serveTokenEndpoint(handler); return yield* use(fixture); }), ); const validCodeBody = { access_token: "tok", token_type: "Bearer", refresh_token: "rtok", expires_in: 3600, scope: "read", }; const validRefreshBody = { access_token: "tok2", token_type: "Bearer", expires_in: 3600 }; const jwtPart = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url"); const unsignedJwt = (claims: Record<string, unknown>, alg = "RS256"): string => `${jwtPart({ alg, typ: "JWT" })}.${jwtPart(claims)}.sig`; const tokenResponse = (body: unknown): TokenHandler => () => json(200, body); const tokenResponseFetch = (body: unknown): typeof globalThis.fetch => async () => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); // --------------------------------------------------------------------------- // PKCE // --------------------------------------------------------------------------- describe("PKCE", () => { it("createPkceCodeVerifier returns a base64url string in the RFC 7636 length range", () => { for (let i = 0; i < 25; i++) { const verifier = createPkceCodeVerifier(); expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/); expect(verifier.length).toBeGreaterThanOrEqual(43); expect(verifier.length).toBeLessThanOrEqual(128); } }); it("createPkceCodeChallenge matches the RFC 7636 Appendix A test vector", async () => { const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"; const expected = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; expect(await createPkceCodeChallenge(verifier)).toBe(expected); }); it("createPkceCodeVerifier produces unique values", () => { const seen = new Set<string>(); for (let i = 0; i < 50; i++) seen.add(createPkceCodeVerifier()); expect(seen.size).toBe(50); }); }); // --------------------------------------------------------------------------- // buildAuthorizationUrl // --------------------------------------------------------------------------- describe("providerAuthorizeExtras (Google offline/consent quirk)", () => { it("adds access_type=offline + prompt=consent for the Google authorize host", () => { expect(providerAuthorizeExtras("https://accounts.google.com/o/oauth2/v2/auth")).toEqual({ access_type: "offline", prompt: "consent", }); }); it("adds nothing for non-Google hosts or an unparseable URL (token host ≠ authorize host)", () => { expect(providerAuthorizeExtras("https://accounts.spotify.com/authorize")).toEqual({}); expect(providerAuthorizeExtras("https://oauth2.googleapis.com/token")).toEqual({}); expect(providerAuthorizeExtras("not a url")).toEqual({}); }); }); describe("buildAuthorizationUrl", () => { const baseInput = { authorizationUrl: "https://example.com/authorize", clientId: "client-123", redirectUrl: "https://app.example.com/callback", scopes: ["read", "write"] as const, state: "state-abc", codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", }; it("emits all RFC 6749 + PKCE params", () => { const url = new URL(buildAuthorizationUrl(baseInput)); expect(url.origin + url.pathname).toBe("https://example.com/authorize"); expect(url.searchParams.get("client_id")).toBe("client-123"); expect(url.searchParams.get("redirect_uri")).toBe("https://app.example.com/callback"); expect(url.searchParams.get("response_type")).toBe("code"); expect(url.searchParams.get("scope")).toBe("read write"); expect(url.searchParams.get("state")).toBe("state-abc"); expect(url.searchParams.get("code_challenge_method")).toBe("S256"); expect(url.searchParams.get("code_challenge")).toBe( "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", ); }); it("supports a custom scope separator (e.g. comma for legacy providers)", () => { const url = new URL(buildAuthorizationUrl({ ...baseInput, scopeSeparator: "," })); expect(url.searchParams.get("scope")).toBe("read,write"); }); it("omits scope when no scopes are requested", () => { const url = new URL(buildAuthorizationUrl({ ...baseInput, scopes: [] })); expect(url.searchParams.has("scope")).toBe(false); }); it("merges provider extra params without dropping them", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, extraParams: { access_type: "offline", prompt: "consent", }, }), ); expect(url.searchParams.get("access_type")).toBe("offline"); expect(url.searchParams.get("prompt")).toBe("consent"); expect(url.searchParams.has("include_granted_scopes")).toBe(false); expect(url.searchParams.get("code_challenge_method")).toBe("S256"); }); it("preserves pre-existing query params on the authorization URL", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, authorizationUrl: "https://example.com/auth?tenant=acme", }), ); expect(url.searchParams.get("tenant")).toBe("acme"); expect(url.searchParams.get("client_id")).toBe("client-123"); }); it("includes RFC 8707 resource indicator when provided", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, resource: "https://api.example.com/v1/mcp", }), ); expect(url.searchParams.get("resource")).toBe("https://api.example.com/v1/mcp"); }); it("omits resource parameter when not provided", () => { const url = new URL(buildAuthorizationUrl(baseInput)); expect(url.searchParams.has("resource")).toBe(false); }); it("rejects unsupported authorization URL schemes", () => { expect(() => buildAuthorizationUrl({ ...baseInput, authorizationUrl: "javascript:alert(1)", }), ).toThrow(/Authorization URL must use https: or loopback http:/); expect(() => buildAuthorizationUrl({ ...baseInput, authorizationUrl: "http://example.com/authorize", }), ).toThrow(/Authorization URL must use https: or loopback http:/); }); it("allows HTTP authorization URLs when the caller opts into local HTTP", () => { const url = new URL( buildAuthorizationUrl({ ...baseInput, authorizationUrl: "http://example.com/authorize", endpointUrlPolicy: { allowHttp: true }, }), ); expect(url.origin + url.pathname).toBe("http://example.com/authorize"); }); }); describe("exchangeAuthorizationCode", () => { it.effect("posts form-urlencoded body with grant_type=authorization_code and PKCE verifier", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", clientSecret: "csecret", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); const call = (yield* calls)[0]!; expect(call.method).toBe("POST"); expect(call.headers["content-type"]).toMatch(/^application\/x-www-form-urlencoded/); expect(call.headers["accept"]).toContain("application/json"); expect(call.body.get("grant_type")).toBe("authorization_code"); expect(call.body.get("client_id")).toBe("cid"); expect(call.body.get("client_secret")).toBe("csecret"); expect(call.body.get("redirect_uri")).toBe("https://app.example.com/cb"); expect(call.body.get("code_verifier")).toBe("verifier"); expect(call.body.get("code")).toBe("abc"); }), ), ); it.effect("rejects unsupported token URL schemes before exchange", () => Effect.gen(function* () { const exit = yield* Effect.exit( exchangeAuthorizationCode({ tokenUrl: "http://example.com/token", clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }), ); expect(Exit.isFailure(exit)).toBe(true); if (!Exit.isFailure(exit)) return; expect(JSON.stringify(exit.cause)).toContain("Token URL must use https: or loopback http:"); }), ); it.effect("omits client_secret when none is provided (public clients with PKCE)", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); const body = (yield* calls)[0]!.body; expect(body.get("client_id")).toBe("cid"); expect(body.has("client_secret")).toBe(false); }), ), ); it.effect("includes RFC 8707 resource parameter on the token request when provided", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", resource: "https://api.example.com/v1/mcp", }); expect((yield* calls)[0]!.body.get("resource")).toBe("https://api.example.com/v1/mcp"); }), ), ); it.effect("omits resource parameter when not provided", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl, calls }) => Effect.gen(function* () { yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect((yield* calls)[0]!.body.has("resource")).toBe(false); }), ), ); it.effect("strips id_tokens whose iss does not match AS metadata", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ iss: "https://us.posthog.com", aud: "cid", sub: "user-1", exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), }), }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, issuerUrl: new URL(tokenUrl).origin, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); expect(result.refresh_token).toBe("rtok"); expect(result.idTokenIdentityLabel).toBe("user-1"); }), ), ); it.effect("extracts id_token email as the identity label", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ email: "alice@example.com", preferred_username: "alice", sub: "user-1", }), }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.idTokenIdentityLabel).toBe("alice@example.com"); }), ), ); it.effect("falls back from id_token email to preferred_username then sub", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ preferred_username: "alice", sub: "user-1", }), }), ({ tokenUrl }) => Effect.gen(function* () { const preferred = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(preferred.idTokenIdentityLabel).toBe("alice"); }), ), ); it("falls back to sub and ignores malformed id_tokens", () => { expect(idTokenIdentityLabel(unsignedJwt({ sub: "user-1" }))).toBe("user-1"); expect(idTokenIdentityLabel("not-a-jwt")).toBeUndefined(); expect(idTokenIdentityLabel(undefined)).toBeUndefined(); }); it.effect("ignores malformed id_tokens without failing the exchange", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: "not-a-jwt", }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); expect(result.idTokenIdentityLabel).toBeUndefined(); }), ), ); it.effect("strips id_tokens whose aud does not match the client_id", () => withTokenEndpoint( tokenResponse({ ...validCodeBody, id_token: unsignedJwt({ iss: "http://127.0.0.1", aud: "another-client", sub: "user-1", exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), }), }), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, issuerUrl: new URL(tokenUrl).origin, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); }), ), ); it.effect("happy path: token endpoint with no id_token still parses normally", () => withTokenEndpoint(tokenResponse(validCodeBody), ({ tokenUrl }) => Effect.gen(function* () { const result = yield* exchangeAuthorizationCode({ tokenUrl, clientId: "cid", redirectUrl: "https://app.example.com/cb", codeVerifier: "verifier", code: "abc", }); expect(result.access_token).toBe("tok"); expect(result.refresh_token).toBe("rtok"); expect(result.expires_in).toBe(3600); expect(result.idTokenIdentityLabel).toBeUndefined(); }), ), ); it.effect("uses nested granted scopes for Slack-style user token responses", () => withTokenEndpoint( tokenResponse({