mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-19 04:08:43 -05:00
fix: redirect hook rejections to errorCallbackURL across auth callback flows (#9702)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@better-auth/sso": patch
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Banned users signing in with an OAuth provider now redirect to the `errorCallbackURL` passed to `signIn.social`, with `?error=BANNED_USER&error_description=<message>` in the query string. Previously the redirect went to the auth server's default error page with `?error=banned`, which broke multi-domain deployments where the auth host and frontend host differ. The `oauth-proxy`, SSO OIDC, and SAML callbacks now also redirect hook rejections to the error URL (previously returned JSON 403), and `oauth-proxy` URL-encodes the `error` query value across all its redirects.
|
||||
@@ -9,6 +9,7 @@ import { handleOAuthUserInfo } from "../../oauth2/link-account";
|
||||
import { 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(),
|
||||
@@ -253,20 +254,28 @@ 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<ReturnType<typeof handleOAuthUserInfo>>;
|
||||
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) {
|
||||
if (isAPIError(e) && e.body?.code) {
|
||||
return 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("_"));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BetterAuthError } from "@better-auth/core/error";
|
||||
import type { GoogleProfile } from "@better-auth/core/social-providers";
|
||||
import { exportJWK, generateKeyPair, SignJWT } from "jose";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
import {
|
||||
@@ -75,6 +76,7 @@ describe("Admin plugin", async () => {
|
||||
customFetchImpl,
|
||||
} = await getTestInstance(
|
||||
{
|
||||
trustedOrigins: ["https://frontend.example.com"],
|
||||
plugins: [
|
||||
admin({
|
||||
bannedUserMessage: "Custom banned user message",
|
||||
@@ -620,7 +622,44 @@ describe("Admin plugin", async () => {
|
||||
},
|
||||
});
|
||||
expect(errorLocation).toBeDefined();
|
||||
expect(errorLocation).toContain("error=banned");
|
||||
expect(errorLocation).toContain("error=BANNED_USER");
|
||||
});
|
||||
|
||||
it("should redirect banned social sign-in to a cross-origin errorCallbackURL", async () => {
|
||||
const errorCallbackURL = "https://frontend.example.com/auth-error";
|
||||
const headers = new Headers();
|
||||
const res = await client.signIn.social(
|
||||
{
|
||||
provider: "google",
|
||||
errorCallbackURL,
|
||||
},
|
||||
{
|
||||
throw: true,
|
||||
onSuccess: cookieSetter(headers),
|
||||
},
|
||||
);
|
||||
const state = new URL(res.url!).searchParams.get("state");
|
||||
let errorLocation: string | null = null;
|
||||
await client.$fetch("/callback/google", {
|
||||
query: {
|
||||
state,
|
||||
code: "test",
|
||||
},
|
||||
headers,
|
||||
method: "GET",
|
||||
onError(context) {
|
||||
expect(context.response.status).toBe(302);
|
||||
errorLocation = context.response.headers.get("location");
|
||||
},
|
||||
});
|
||||
expect(errorLocation).toBeTruthy();
|
||||
const url = new URL(errorLocation!);
|
||||
expect(url.origin).toBe("https://frontend.example.com");
|
||||
expect(`${url.origin}${url.pathname}`).toBe(errorCallbackURL);
|
||||
expect(url.searchParams.get("error")).toBe("BANNED_USER");
|
||||
expect(url.searchParams.get("error_description")).toBe(
|
||||
"Custom banned user message",
|
||||
);
|
||||
});
|
||||
|
||||
it("should change banned user message", async () => {
|
||||
@@ -1935,3 +1974,67 @@ describe("edge cases: userId validation", async () => {
|
||||
).rejects.toThrow("user not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Admin plugin id-token sign-in", async () => {
|
||||
const googleKeyPair = await generateKeyPair("RS256");
|
||||
const googleJwk = await exportJWK(googleKeyPair.publicKey);
|
||||
const googleKid = "test-admin-google-kid";
|
||||
googleJwk.kid = googleKid;
|
||||
googleJwk.alg = "RS256";
|
||||
googleJwk.use = "sig";
|
||||
|
||||
const clientId = "admin-idtoken-client.googleusercontent.com";
|
||||
|
||||
const signIdToken = (email: string) =>
|
||||
new SignJWT({
|
||||
email,
|
||||
email_verified: true,
|
||||
name: "Banned ID Token User",
|
||||
sub: "banned-google-sub",
|
||||
})
|
||||
.setProtectedHeader({ alg: "RS256", kid: googleKid })
|
||||
.setIssuedAt()
|
||||
.setIssuer("https://accounts.google.com")
|
||||
.setAudience(clientId)
|
||||
.setExpirationTime("1h")
|
||||
.sign(googleKeyPair.privateKey);
|
||||
|
||||
beforeAll(() => {
|
||||
server.use(
|
||||
http.get("https://www.googleapis.com/oauth2/v3/certs", () =>
|
||||
HttpResponse.json({ keys: [googleJwk] }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return BANNED_USER 403 JSON for id-token sign-in by banned user", async () => {
|
||||
const { client } = await getTestInstance(
|
||||
{
|
||||
plugins: [admin({ bannedUserMessage: "Custom banned user message" })],
|
||||
socialProviders: {
|
||||
google: { clientId, clientSecret: "test-secret" },
|
||||
},
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (user) => ({
|
||||
data: { ...user, emailVerified: true, banned: true },
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ disableTestUser: true },
|
||||
);
|
||||
|
||||
const idToken = await signIdToken("banned-idtoken@test.com");
|
||||
const res = await client.signIn.social({
|
||||
provider: "google",
|
||||
idToken: { token: idToken },
|
||||
});
|
||||
|
||||
expect(res.error?.status).toBe(403);
|
||||
expect(res.error?.code).toBe("BANNED_USER");
|
||||
expect(res.error?.message).toBe("Custom banned user message");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,19 +113,6 @@ export const admin = <O extends AdminOptions>(options?: O | undefined) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
ctx &&
|
||||
(ctx.path.startsWith("/callback") ||
|
||||
ctx.path.startsWith("/oauth2/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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GenericEndpointContext } from "@better-auth/core";
|
||||
import { runWithEndpointContext } from "@better-auth/core/context";
|
||||
import { APIError } from "@better-auth/core/error";
|
||||
import { betterFetch } from "@better-fetch/fetch";
|
||||
import { OAuth2Server } from "oauth2-mock-server";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
@@ -486,6 +487,82 @@ describe("oauth2", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should redirect to cross-origin errorCallbackURL when a session hook throws APIError", async () => {
|
||||
server.service.once("beforeUserinfo", (userInfoResponse) => {
|
||||
userInfoResponse.body = {
|
||||
email: "hook-reject@test.com",
|
||||
name: "Hook Reject User",
|
||||
sub: "hook-reject",
|
||||
picture: "https://test.com/picture.png",
|
||||
email_verified: true,
|
||||
};
|
||||
userInfoResponse.statusCode = 200;
|
||||
});
|
||||
|
||||
const { customFetchImpl, cookieSetter } = await getTestInstance(
|
||||
{
|
||||
trustedOrigins: ["https://frontend.example.com"],
|
||||
plugins: [
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: "test-hook-reject",
|
||||
discoveryUrl: `http://localhost:${port}/.well-known/openid-configuration`,
|
||||
clientId,
|
||||
clientSecret,
|
||||
pkce: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (user) => ({
|
||||
data: { ...user, emailVerified: true },
|
||||
}),
|
||||
},
|
||||
},
|
||||
session: {
|
||||
create: {
|
||||
before: async () => {
|
||||
throw APIError.from("FORBIDDEN", {
|
||||
code: "HOOK_REJECTED",
|
||||
message: "Session hook rejected this user",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ disableTestUser: true },
|
||||
);
|
||||
const authClient = createAuthClient({
|
||||
plugins: [genericOAuthClient()],
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: { customFetchImpl },
|
||||
});
|
||||
const headers = new Headers();
|
||||
const res = await authClient.signIn.oauth2({
|
||||
providerId: "test-hook-reject",
|
||||
callbackURL: "https://frontend.example.com/dashboard",
|
||||
errorCallbackURL: "https://frontend.example.com/auth-error",
|
||||
fetchOptions: { onSuccess: cookieSetter(headers) },
|
||||
});
|
||||
const { callbackURL } = await simulateOAuthFlow(
|
||||
res.data?.url || "",
|
||||
headers,
|
||||
customFetchImpl,
|
||||
);
|
||||
const url = new URL(callbackURL);
|
||||
expect(url.origin).toBe("https://frontend.example.com");
|
||||
expect(url.pathname).toBe("/auth-error");
|
||||
expect(url.searchParams.get("error")).toBe("HOOK_REJECTED");
|
||||
expect(url.searchParams.get("error_description")).toBe(
|
||||
"Session hook rejected this user",
|
||||
);
|
||||
});
|
||||
|
||||
it("should create user when sign ups are disabled and sign up is requested", async () => {
|
||||
server.service.once("beforeUserinfo", (userInfoResponse) => {
|
||||
userInfoResponse.body = {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { generateState, parseState } from "../../oauth2/state";
|
||||
import { setTokenUtil } from "../../oauth2/utils";
|
||||
import type { User } from "../../types";
|
||||
import { HIDE_METADATA } from "../../utils";
|
||||
import { isAPIError } from "../../utils/is-api-error";
|
||||
import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes";
|
||||
import type { GenericOAuthOptions } from "./types";
|
||||
|
||||
@@ -315,17 +316,15 @@ export const oAuth2Callback = (options: GenericOAuthOptions) =>
|
||||
} = parsedState;
|
||||
const code = ctx.query.code;
|
||||
|
||||
function redirectOnError(error: string) {
|
||||
function redirectOnError(error: string, description?: string) {
|
||||
const defaultErrorURL =
|
||||
ctx.context.options.onAPIError?.errorURL ||
|
||||
`${ctx.context.baseURL}/error`;
|
||||
let url = errorURL || defaultErrorURL;
|
||||
if (url.includes("?")) {
|
||||
url = `${url}&error=${encodeURIComponent(error)}`;
|
||||
} else {
|
||||
url = `${url}?error=${encodeURIComponent(error)}`;
|
||||
}
|
||||
throw ctx.redirect(url);
|
||||
const baseURL = errorURL || defaultErrorURL;
|
||||
const params = new URLSearchParams({ error });
|
||||
if (description) params.set("error_description", description);
|
||||
const sep = baseURL.includes("?") ? "&" : "?";
|
||||
throw ctx.redirect(`${baseURL}${sep}${params.toString()}`);
|
||||
}
|
||||
|
||||
let finalTokenUrl = providerConfig.tokenUrl;
|
||||
@@ -514,20 +513,28 @@ export const oAuth2Callback = (options: GenericOAuthOptions) =>
|
||||
throw ctx.redirect(toRedirectTo);
|
||||
}
|
||||
|
||||
const result = await handleOAuthUserInfo(ctx, {
|
||||
userInfo,
|
||||
account: {
|
||||
providerId: providerConfig.providerId,
|
||||
accountId: userInfo.id,
|
||||
...tokens,
|
||||
scope: tokens.scopes?.join(","),
|
||||
},
|
||||
callbackURL: callbackURL,
|
||||
disableSignUp:
|
||||
(providerConfig.disableImplicitSignUp && !requestSignUp) ||
|
||||
providerConfig.disableSignUp,
|
||||
overrideUserInfo: providerConfig.overrideUserInfo,
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof handleOAuthUserInfo>>;
|
||||
try {
|
||||
result = await handleOAuthUserInfo(ctx, {
|
||||
userInfo,
|
||||
account: {
|
||||
providerId: providerConfig.providerId,
|
||||
accountId: userInfo.id,
|
||||
...tokens,
|
||||
scope: tokens.scopes?.join(","),
|
||||
},
|
||||
callbackURL: callbackURL,
|
||||
disableSignUp:
|
||||
(providerConfig.disableImplicitSignUp && !requestSignUp) ||
|
||||
providerConfig.disableSignUp,
|
||||
overrideUserInfo: providerConfig.overrideUserInfo,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isAPIError(e) && e.body?.code) {
|
||||
return redirectOnError(e.body.code, e.body.message);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
return redirectOnError(result.error.split(" ").join("_"));
|
||||
|
||||
@@ -19,6 +19,7 @@ import { handleOAuthUserInfo } from "../../oauth2/link-account";
|
||||
import type { StateData } from "../../state";
|
||||
import { parseGenericState } from "../../state";
|
||||
import type { Account, User } from "../../types";
|
||||
import { isAPIError } from "../../utils/is-api-error";
|
||||
import { getOrigin } from "../../utils/url";
|
||||
import { PACKAGE_VERSION } from "../../version";
|
||||
import {
|
||||
@@ -242,12 +243,20 @@ export const oAuthProxy = <O extends OAuthProxyOptions>(opts?: O) => {
|
||||
ctx.context.logger.warn("Failed to clean up OAuth state", e);
|
||||
}
|
||||
|
||||
const result = await handleOAuthUserInfo(ctx, {
|
||||
userInfo: payload.userInfo,
|
||||
account: payload.account,
|
||||
callbackURL: payload.callbackURL,
|
||||
disableSignUp: payload.disableSignUp,
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof handleOAuthUserInfo>>;
|
||||
try {
|
||||
result = await handleOAuthUserInfo(ctx, {
|
||||
userInfo: payload.userInfo,
|
||||
account: payload.account,
|
||||
callbackURL: payload.callbackURL,
|
||||
disableSignUp: payload.disableSignUp,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isAPIError(e) && e.body?.code) {
|
||||
throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (result.error || !result.data) {
|
||||
ctx.context.logger.error(
|
||||
"Failed to create user or session",
|
||||
|
||||
@@ -79,7 +79,10 @@ export function redirectOnError(
|
||||
ctx: GenericEndpointContext,
|
||||
errorURL: string,
|
||||
error: string,
|
||||
description?: string,
|
||||
): never {
|
||||
const params = new URLSearchParams({ error });
|
||||
if (description) params.set("error_description", description);
|
||||
const sep = errorURL.includes("?") ? "&" : "?";
|
||||
throw ctx.redirect(`${errorURL}${sep}error=${error}`);
|
||||
throw ctx.redirect(`${errorURL}${sep}${params.toString()}`);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { APIError } from "@better-auth/core/error";
|
||||
import { betterFetch } from "@better-fetch/fetch";
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { organization } from "better-auth/plugins";
|
||||
@@ -1837,3 +1838,125 @@ describe("SSO OIDC UserInfo endpoint sub claim mapping", async () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SSO OIDC hook rejection redirect", async () => {
|
||||
const hookServer = new OAuth2Server();
|
||||
|
||||
beforeAll(async () => {
|
||||
await hookServer.issuer.keys.generate("RS256");
|
||||
await hookServer.start(8090, "localhost");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await hookServer.stop().catch(() => {});
|
||||
});
|
||||
|
||||
hookServer.service.on("beforeUserinfo", (userInfoResponse) => {
|
||||
userInfoResponse.body = {
|
||||
email: "rejected@test.com",
|
||||
name: "Rejected User",
|
||||
sub: "rejected-sub",
|
||||
email_verified: true,
|
||||
};
|
||||
userInfoResponse.statusCode = 200;
|
||||
});
|
||||
|
||||
hookServer.service.on("beforeTokenSigning", (token) => {
|
||||
token.payload.email = "rejected@test.com";
|
||||
token.payload.email_verified = true;
|
||||
});
|
||||
|
||||
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
|
||||
await getTestInstance({
|
||||
trustedOrigins: ["http://localhost:8090", "https://frontend.example.com"],
|
||||
plugins: [sso()],
|
||||
databaseHooks: {
|
||||
session: {
|
||||
create: {
|
||||
before: async (session, ctx) => {
|
||||
if (!ctx) return;
|
||||
const user = await ctx.context.internalAdapter.findUserById(
|
||||
session.userId,
|
||||
);
|
||||
if (user?.email === "rejected@test.com") {
|
||||
throw APIError.from("FORBIDDEN", {
|
||||
code: "HOOK_REJECTED",
|
||||
message: "SSO hook rejected this user",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authClient = createAuthClient({
|
||||
plugins: [ssoClient()],
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: { customFetchImpl },
|
||||
});
|
||||
|
||||
it("should redirect to cross-origin errorCallbackURL when a session hook throws APIError", async () => {
|
||||
const { headers: adminHeaders } = await signInWithTestUser();
|
||||
await auth.api.registerSSOProvider({
|
||||
body: {
|
||||
issuer: hookServer.issuer.url!,
|
||||
domain: "hook-reject.com",
|
||||
providerId: "hook-reject",
|
||||
oidcConfig: {
|
||||
clientId: "test",
|
||||
clientSecret: "test",
|
||||
authorizationEndpoint: `${hookServer.issuer.url}/authorize`,
|
||||
tokenEndpoint: `${hookServer.issuer.url}/token`,
|
||||
jwksEndpoint: `${hookServer.issuer.url}/jwks`,
|
||||
discoveryEndpoint: `${hookServer.issuer.url}/.well-known/openid-configuration`,
|
||||
mapping: {
|
||||
id: "sub",
|
||||
email: "email",
|
||||
emailVerified: "email_verified",
|
||||
name: "name",
|
||||
},
|
||||
},
|
||||
},
|
||||
headers: adminHeaders,
|
||||
});
|
||||
|
||||
const signInHeaders = new Headers();
|
||||
const res = await authClient.signIn.sso({
|
||||
providerId: "hook-reject",
|
||||
callbackURL: "https://frontend.example.com/dashboard",
|
||||
errorCallbackURL: "https://frontend.example.com/auth-error",
|
||||
fetchOptions: {
|
||||
throw: true,
|
||||
onSuccess: cookieSetter(signInHeaders),
|
||||
},
|
||||
});
|
||||
|
||||
let location: string | null = null;
|
||||
await betterFetch(res.url, {
|
||||
method: "GET",
|
||||
redirect: "manual",
|
||||
onError(context) {
|
||||
location = context.response.headers.get("location");
|
||||
},
|
||||
});
|
||||
|
||||
let callbackURL = "";
|
||||
await betterFetch(location!, {
|
||||
method: "GET",
|
||||
customFetchImpl,
|
||||
headers: signInHeaders,
|
||||
onError(context) {
|
||||
callbackURL = context.response.headers.get("location") || "";
|
||||
},
|
||||
});
|
||||
|
||||
const url = new URL(callbackURL);
|
||||
expect(url.origin).toBe("https://frontend.example.com");
|
||||
expect(url.pathname).toBe("/auth-error");
|
||||
expect(url.searchParams.get("error")).toBe("HOOK_REJECTED");
|
||||
expect(url.searchParams.get("error_description")).toBe(
|
||||
"SSO hook rejected this user",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isAPIError } from "@better-auth/core/utils/is-api-error";
|
||||
import type { User } from "better-auth";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { setSessionCookie } from "better-auth/cookies";
|
||||
@@ -427,24 +428,36 @@ export async function processSAMLResponse(
|
||||
relayState?.callbackURL ||
|
||||
parsedSamlConfig.callbackUrl ||
|
||||
ctx.context.baseURL;
|
||||
const errorUrl = relayState?.errorURL || samlRedirectUrl;
|
||||
|
||||
const result = await handleOAuthUserInfo(ctx, {
|
||||
userInfo: {
|
||||
email: userInfo.email as string,
|
||||
name: (userInfo.name || userInfo.email) as string,
|
||||
id: userInfo.id as string,
|
||||
emailVerified: Boolean(userInfo.emailVerified),
|
||||
},
|
||||
account: {
|
||||
providerId,
|
||||
accountId: userInfo.id as string,
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
},
|
||||
callbackURL: callbackUrl,
|
||||
disableSignUp: options?.disableImplicitSignUp,
|
||||
isTrustedProvider,
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof handleOAuthUserInfo>>;
|
||||
try {
|
||||
result = await handleOAuthUserInfo(ctx, {
|
||||
userInfo: {
|
||||
email: userInfo.email as string,
|
||||
name: (userInfo.name || userInfo.email) as string,
|
||||
id: userInfo.id as string,
|
||||
emailVerified: Boolean(userInfo.emailVerified),
|
||||
},
|
||||
account: {
|
||||
providerId,
|
||||
accountId: userInfo.id as string,
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
},
|
||||
callbackURL: callbackUrl,
|
||||
disableSignUp: options?.disableImplicitSignUp,
|
||||
isTrustedProvider,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isAPIError(e) && e.body?.code) {
|
||||
const params = new URLSearchParams({ error: e.body.code });
|
||||
if (e.body.message) params.set("error_description", e.body.message);
|
||||
const sep = errorUrl.includes("?") ? "&" : "?";
|
||||
throw ctx.redirect(`${errorUrl}${sep}${params.toString()}`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
throw ctx.redirect(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isAPIError } from "@better-auth/core/utils/is-api-error";
|
||||
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
||||
import {
|
||||
createAuthorizationURL,
|
||||
@@ -1654,31 +1655,43 @@ async function handleOIDCCallback(
|
||||
(provider as { domainVerified?: boolean }).domainVerified === true &&
|
||||
validateEmailDomain(userInfo.email, provider.domain);
|
||||
|
||||
const linked = await handleOAuthUserInfo(ctx, {
|
||||
userInfo: {
|
||||
email: userInfo.email,
|
||||
name: userInfo.name || "",
|
||||
id: userInfo.id,
|
||||
image: userInfo.image,
|
||||
emailVerified: options?.trustEmailVerified
|
||||
? userInfo.emailVerified || false
|
||||
: false,
|
||||
},
|
||||
account: {
|
||||
idToken: tokenResponse.idToken,
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
accountId: userInfo.id,
|
||||
providerId: provider.providerId,
|
||||
accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
|
||||
scope: tokenResponse.scopes?.join(","),
|
||||
},
|
||||
callbackURL,
|
||||
disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
|
||||
overrideUserInfo: config.overrideUserInfo,
|
||||
isTrustedProvider,
|
||||
});
|
||||
let linked: Awaited<ReturnType<typeof handleOAuthUserInfo>>;
|
||||
try {
|
||||
linked = await handleOAuthUserInfo(ctx, {
|
||||
userInfo: {
|
||||
email: userInfo.email,
|
||||
name: userInfo.name || "",
|
||||
id: userInfo.id,
|
||||
image: userInfo.image,
|
||||
emailVerified: options?.trustEmailVerified
|
||||
? userInfo.emailVerified || false
|
||||
: false,
|
||||
},
|
||||
account: {
|
||||
idToken: tokenResponse.idToken,
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
accountId: userInfo.id,
|
||||
providerId: provider.providerId,
|
||||
accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
|
||||
scope: tokenResponse.scopes?.join(","),
|
||||
},
|
||||
callbackURL,
|
||||
disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
|
||||
overrideUserInfo: config.overrideUserInfo,
|
||||
isTrustedProvider,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isAPIError(e) && e.body?.code) {
|
||||
const baseURL = errorURL || callbackURL;
|
||||
const params = new URLSearchParams({ error: e.body.code });
|
||||
if (e.body.message) params.set("error_description", e.body.message);
|
||||
const sep = baseURL.includes("?") ? "&" : "?";
|
||||
throw ctx.redirect(`${baseURL}${sep}${params.toString()}`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (linked.error) {
|
||||
throw ctx.redirect(`${errorURL || callbackURL}?error=${linked.error}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user