fix(oauth-proxy): forward result.error verbatim in callback redirect (#9723)

This commit is contained in:
Taesu
2026-05-22 02:52:48 +00:00
committed by GitHub
parent f47aa4aa96
commit 015f96bc63
3 changed files with 90 additions and 2 deletions
@@ -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.
@@ -257,11 +257,21 @@ export const oAuthProxy = <O extends OAuthProxyOptions>(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");
}
@@ -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: [