From 16a8bfa1fabe34912e4a028f3521980b8bd15be7 Mon Sep 17 00:00:00 2001 From: Bekacru <86073083+Bekacru@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:15:15 -0700 Subject: [PATCH] test(oauth): cover exchange-throw logging and guard against raw token leaks --- .../src/api/routes/callback.test.ts | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/better-auth/src/api/routes/callback.test.ts b/packages/better-auth/src/api/routes/callback.test.ts index 0857003771..3fa9390ed4 100644 --- a/packages/better-auth/src/api/routes/callback.test.ts +++ b/packages/better-auth/src/api/routes/callback.test.ts @@ -17,7 +17,11 @@ describe("callback", () => { validateAuthorizationCode: async () => // Mirrors a provider (e.g. Slack) that answers with HTTP 200 but an // error in the response body instead of failing the HTTP request. - getOAuth2Tokens({ ok: false, error: "invalid_code" }), + getOAuth2Tokens({ + ok: false, + error: "invalid_code", + refresh_token: "sensitive-refresh-token", + }), getUserInfo: async () => null, } satisfies OAuthProvider>; @@ -70,6 +74,66 @@ describe("callback", () => { }); // The raw token response must never be logged as-is. expect(JSON.stringify(noTokenCalls[0])).not.toContain('"raw"'); + expect(JSON.stringify(noTokenCalls[0])).not.toContain( + "sensitive-refresh-token", + ); + + errorSpy.mockRestore(); + }); + + it("logs the provider id when the authorization code exchange throws", async () => { + const exchangeError = new Error("token endpoint unreachable"); + const provider = { + id: "throwing-provider", + name: "Throwing Provider", + accountSubject: () => "subject", + createAuthorizationURL: ({ state }) => + new URL(`https://idp.example.com/authorize?state=${state}`), + validateAuthorizationCode: async () => { + throw exchangeError; + }, + getUserInfo: async () => null, + } satisfies OAuthProvider>; + + const { client, auth, cookieSetter } = await getTestInstance({ + plugins: [ + { + id: "throwing-provider-plugin", + init: (ctx) => ({ + context: { + socialProviders: [provider, ...ctx.socialProviders], + }, + }), + }, + ], + }); + + const ctx = await auth.$context; + const errorSpy = vi.spyOn(ctx.logger, "error").mockImplementation(() => {}); + + const oAuthHeaders = new Headers(); + const signIn = await client.signIn.social( + { + provider: provider.id, + callbackURL: "/dashboard", + }, + { + throw: true, + onSuccess: cookieSetter(oAuthHeaders), + }, + ); + const state = new URL(signIn.url!).searchParams.get("state"); + + await client.$fetch(`/callback/${provider.id}?code=test&state=${state}`, { + method: "GET", + headers: oAuthHeaders, + onError() {}, + }); + + expect(errorSpy).toHaveBeenCalledWith( + "Failed to exchange OAuth authorization code", + { providerId: provider.id, error: exchangeError }, + ); errorSpy.mockRestore(); });