From d309e5d2baf07bcdc86c09d846fca5e0ed72258d Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Sat, 30 May 2026 23:00:10 +0100 Subject: [PATCH] fix(oauth): forward hook errors and sync linked profiles on rewritten callback (#9702, #9788, #8758) Re-applies three main fixes onto next's rewritten OAuth callback, which now routes generic-oauth through the shared callback route. - #9702/#9788: a before-callback hook that rejects sign-in (the banned-user guard is the flagship case) now redirects to the per-flow errorURL carrying the hook's machine-readable code and message, instead of surfacing a raw error response. handleOAuthUserInfo is wrapped in try/catch; the hook's APIError code is forwarded verbatim (it is app-defined, not a member of OAUTH_CALLBACK_ERROR_CODES) via next's local redirectOnError closure. The admin plugin's stale inline /callback redirect is removed so the banned-user path flows through that single forwarder and emits BANNED_USER with a URL-encoded description. - #8758: accountLinking.updateUserInfoOnLink now applies on the explicit OAuth link path, not only id-token linking. The regression tests (banned social sign-in emits error=BANNED_USER, the cross-origin errorCallbackURL case, and the 403-JSON id-token case) arrived via the merge and were red on the baseline; these source changes make them pass. (cherry picked from commit 1d893ae572277447025e29462946a7aeb11443ac) --- .../better-auth/src/api/routes/callback.ts | 48 +++++++++++++------ .../better-auth/src/plugins/admin/admin.ts | 9 ---- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/better-auth/src/api/routes/callback.ts b/packages/better-auth/src/api/routes/callback.ts index 22a8bd5b43..319c6c25b0 100644 --- a/packages/better-auth/src/api/routes/callback.ts +++ b/packages/better-auth/src/api/routes/callback.ts @@ -6,10 +6,14 @@ import { getAwaitableValue } from "../../context/helpers"; import { setSessionCookie } from "../../cookies"; import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/error-codes"; import { missingEmailLogMessage } from "../../oauth2/errors"; -import { handleOAuthUserInfo } from "../../oauth2/link-account"; +import { + applyUpdateUserInfoOnLink, + handleOAuthUserInfo, +} from "../../oauth2/link-account"; import { generateState, parseState } from "../../oauth2/state"; import { setTokenUtil } from "../../oauth2/utils"; import { HIDE_METADATA } from "../../utils/hide-metadata"; +import { isAPIError } from "../../utils/is-api-error"; const schema = z.object({ code: z.string().optional(), @@ -274,6 +278,7 @@ export const callbackOAuth = createAuthEndpoint( ); } } + await applyUpdateUserInfoOnLink(c, link.userId, userInfo); let toRedirectTo: string; try { const url = callbackURL; @@ -294,20 +299,33 @@ export const callbackOAuth = createAuthEndpoint( ...tokens, scope: tokens.scopes?.join(","), }; - const result = await handleOAuthUserInfo(c, { - userInfo: { - ...userInfo, - id: providerAccountId, - email: userInfo.email, - name: userInfo.name || "", - }, - account: accountData, - callbackURL, - disableSignUp: - (provider.disableImplicitSignUp && !requestSignUp) || - provider.options?.disableSignUp, - overrideUserInfo: provider.options?.overrideUserInfoOnSignIn, - }); + let result: Awaited>; + try { + result = await handleOAuthUserInfo(c, { + userInfo: { + ...userInfo, + id: providerAccountId, + email: userInfo.email, + name: userInfo.name || "", + }, + account: accountData, + callbackURL, + disableSignUp: + (provider.disableImplicitSignUp && !requestSignUp) || + provider.options?.disableSignUp, + overrideUserInfo: provider.options?.overrideUserInfoOnSignIn, + }); + } catch (e) { + // A before-callback hook (for example the admin plugin's banned-user + // guard) can reject sign-in by throwing an APIError. Forward its + // machine-readable code and message to the per-flow errorURL instead + // of surfacing a raw error response. The code is app-defined, so it is + // forwarded verbatim rather than mapped onto OAUTH_CALLBACK_ERROR_CODES. + if (isAPIError(e) && e.body?.code) { + redirectOnError(e.body.code, e.body.message); + } + throw e; + } if (result.error) { c.context.logger.error(result.error.split(" ").join("_")); return redirectOnError(result.error.split(" ").join("_")); diff --git a/packages/better-auth/src/plugins/admin/admin.ts b/packages/better-auth/src/plugins/admin/admin.ts index bbdaa86515..1032ee26f6 100644 --- a/packages/better-auth/src/plugins/admin/admin.ts +++ b/packages/better-auth/src/plugins/admin/admin.ts @@ -113,15 +113,6 @@ export const admin = (options?: O | undefined) => { return; } - if (ctx.path.startsWith("/callback")) { - const redirectURI = - ctx.context.options.onAPIError?.errorURL || - `${ctx.context.baseURL}/error`; - throw ctx.redirect( - `${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`, - ); - } - throw APIError.from("FORBIDDEN", { message: opts.bannedUserMessage, code: "BANNED_USER",