diff --git a/.changeset/oauth-proxy-forward-error-code.md b/.changeset/oauth-proxy-forward-error-code.md new file mode 100644 index 0000000000..18e35652a3 --- /dev/null +++ b/.changeset/oauth-proxy-forward-error-code.md @@ -0,0 +1,5 @@ +--- +"better-auth": patch +--- + +The `oauth-proxy` callback now forwards `result.error` from `handleOAuthUserInfo` as the `?error=` query value (e.g. `?error=signup_disabled`) instead of collapsing every error into a generic `?error=user_creation_failed`. Matches the behavior of the core OAuth callback and generic-oauth. The `user_creation_failed` value is still used as a fallback when `result.data` is missing without an explicit error. diff --git a/packages/better-auth/src/plugins/oauth-proxy/index.ts b/packages/better-auth/src/plugins/oauth-proxy/index.ts index d2f2d0c4a7..df3d9bb7c8 100644 --- a/packages/better-auth/src/plugins/oauth-proxy/index.ts +++ b/packages/better-auth/src/plugins/oauth-proxy/index.ts @@ -257,11 +257,21 @@ export const oAuthProxy = (opts?: O) => { } throw e; } - if (result.error || !result.data) { + if (result.error) { ctx.context.logger.error( - "Failed to create user or session", + "OAuth proxy callback error", result.error, ); + throw redirectOnError( + ctx, + errorURL, + result.error.split(" ").join("_"), + ); + } + if (!result.data) { + ctx.context.logger.error( + "OAuth proxy callback missing session data", + ); throw redirectOnError(ctx, errorURL, "user_creation_failed"); } diff --git a/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts b/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts index 426c2c11c5..b475a19369 100644 --- a/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts +++ b/packages/better-auth/src/plugins/oauth-proxy/oauth-proxy.test.ts @@ -615,6 +615,79 @@ describe("oauth-proxy", async () => { expect(previewSessions.length).toBe(1); }); + it("should forward result.error verbatim instead of collapsing to user_creation_failed", async () => { + const production = await getTestInstance( + { + plugins: [ + oAuthProxy({ + currentURL: "http://preview.example.com", + }), + ], + socialProviders: { + google: { + clientId: "test", + clientSecret: "test", + disableSignUp: true, + }, + }, + }, + { disableTestUser: true }, + ); + + const preview = await getTestInstance( + { + baseURL: "http://preview.example.com", + plugins: [oAuthProxy()], + socialProviders: { + google: { + clientId: "test", + clientSecret: "test", + }, + }, + }, + { disableTestUser: true }, + ); + + const res = await production.client.signIn.social( + { + provider: "google", + callbackURL: "/dashboard", + }, + { throw: true }, + ); + const state = new URL(res.url!).searchParams.get("state"); + + let encryptedProfile: string | null = null; + let callbackURL: string | null = null; + await production.client.$fetch( + `/callback/google?code=test&state=${state}`, + { + onError(context) { + const location = context.response.headers.get("location"); + if (location && location.includes("profile=")) { + const url = new URL(location); + encryptedProfile = url.searchParams.get("profile"); + callbackURL = url.searchParams.get("callbackURL"); + } + }, + }, + ); + expect(encryptedProfile).toBeTruthy(); + + let proxyRedirect: string | null = null; + await preview.client.$fetch( + `/oauth-proxy-callback?callbackURL=${encodeURIComponent(callbackURL!)}&profile=${encodeURIComponent(encryptedProfile!)}`, + { + onError(context) { + proxyRedirect = context.response.headers.get("location"); + }, + }, + ); + expect(proxyRedirect).toBeTruthy(); + const url = new URL(proxyRedirect!); + expect(url.searchParams.get("error")).toBe("signup_disabled"); + }); + it("should reject expired profile payloads", async () => { const { client, auth } = await getTestInstance({ plugins: [