@zpg6-test-pkgs/better-auth
Version:
The most comprehensive authentication library for TypeScript.
706 lines (703 loc) • 25.4 kB
JavaScript
import { betterFetch } from '@better-fetch/fetch';
import { APIError } from 'better-call';
import { decodeJwt } from 'jose';
import * as z from 'zod/v4';
import { g as generateState, c as createAuthorizationURL, p as parseState, v as validateAuthorizationCode, j as handleOAuthUserInfo, r as refreshAccessToken } from '../../shared/better-auth.D7aTFyWE.mjs';
import { a as createAuthEndpoint, s as sessionMiddleware, B as BASE_ERROR_CODES } from '../../shared/better-auth.BfeJWAMn.mjs';
import { s as setSessionCookie } from '../../shared/better-auth.DF-MUmVw.mjs';
import '../../shared/better-auth.n2KFGwjY.mjs';
import '../../shared/better-auth.CMQ3rA-I.mjs';
import '../../shared/better-auth.BjBlybv-.mjs';
import '@better-auth/utils/hash';
import '@better-auth/utils/base64';
import '@noble/ciphers/chacha';
import '@noble/ciphers/utils';
import '@noble/ciphers/webcrypto';
import '@noble/hashes/scrypt';
import '@better-auth/utils';
import '@better-auth/utils/hex';
import '@noble/hashes/utils';
import '../../shared/better-auth.B4Qoxdgc.mjs';
import '../../shared/better-auth.CW6D9eSx.mjs';
import '../../crypto/index.mjs';
import '../../shared/better-auth.CuS_eDdK.mjs';
import '../../shared/better-auth.DdzSJf-n.mjs';
import 'jose/errors';
import '@better-auth/utils/random';
import '../../shared/better-auth.BZZKN1g7.mjs';
import '../../shared/better-auth.BUPPRXfK.mjs';
import '@better-auth/utils/hmac';
import '@better-auth/utils/binary';
import 'defu';
async function getUserInfo(tokens, finalUserInfoUrl) {
if (tokens.idToken) {
const decoded = decodeJwt(tokens.idToken);
if (decoded) {
if (decoded.sub && decoded.email) {
return {
id: decoded.sub,
emailVerified: decoded.email_verified,
image: decoded.picture,
...decoded
};
}
}
}
if (!finalUserInfoUrl) {
return null;
}
const userInfo = await betterFetch(finalUserInfoUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
return {
// @ts-expect-error sub is optional in the type
id: userInfo.data?.sub,
emailVerified: userInfo.data?.email_verified ?? false,
email: userInfo.data?.email,
image: userInfo.data?.picture,
name: userInfo.data?.name,
...userInfo.data
};
}
const genericOAuth = (options) => {
const ERROR_CODES = {
INVALID_OAUTH_CONFIGURATION: "Invalid OAuth configuration"
};
return {
id: "generic-oauth",
init: (ctx) => {
const genericProviders = options.config.map((c) => {
let finalUserInfoUrl = c.userInfoUrl;
return {
id: c.providerId,
name: c.providerId,
createAuthorizationURL(data) {
return createAuthorizationURL({
id: c.providerId,
options: {
clientId: c.clientId,
clientSecret: c.clientSecret,
redirectURI: c.redirectURI
},
authorizationEndpoint: c.authorizationUrl,
state: data.state,
codeVerifier: c.pkce ? data.codeVerifier : void 0,
scopes: c.scopes || [],
redirectURI: `${ctx.baseURL}/oauth2/callback/${c.providerId}`
});
},
async validateAuthorizationCode(data) {
let finalTokenUrl = c.tokenUrl;
if (c.discoveryUrl) {
const discovery = await betterFetch(c.discoveryUrl, {
method: "GET",
headers: c.discoveryHeaders
});
if (discovery.data) {
finalTokenUrl = discovery.data.token_endpoint;
finalUserInfoUrl = discovery.data.userinfo_endpoint;
}
}
if (!finalTokenUrl) {
throw new APIError("BAD_REQUEST", {
message: "Invalid OAuth configuration. Token URL not found."
});
}
return validateAuthorizationCode({
headers: c.authorizationHeaders,
code: data.code,
codeVerifier: data.codeVerifier,
redirectURI: data.redirectURI,
options: {
clientId: c.clientId,
clientSecret: c.clientSecret,
redirectURI: c.redirectURI
},
tokenEndpoint: finalTokenUrl,
authentication: c.authentication
});
},
async refreshAccessToken(refreshToken) {
let finalTokenUrl = c.tokenUrl;
if (c.discoveryUrl) {
const discovery = await betterFetch(c.discoveryUrl, {
method: "GET",
headers: c.discoveryHeaders
});
if (discovery.data) {
finalTokenUrl = discovery.data.token_endpoint;
}
}
if (!finalTokenUrl) {
throw new APIError("BAD_REQUEST", {
message: "Invalid OAuth configuration. Token URL not found."
});
}
return refreshAccessToken({
refreshToken,
options: {
clientId: c.clientId,
clientSecret: c.clientSecret
},
authentication: c.authentication,
tokenEndpoint: finalTokenUrl
});
},
async getUserInfo(tokens) {
const userInfo = c.getUserInfo ? await c.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
if (!userInfo) {
return null;
}
return {
user: {
id: userInfo?.id,
email: userInfo?.email,
emailVerified: userInfo?.emailVerified,
image: userInfo?.image,
name: userInfo?.name,
...c.mapProfileToUser?.(userInfo)
},
data: userInfo
};
}
};
});
return {
context: {
socialProviders: genericProviders.concat(ctx.socialProviders)
}
};
},
endpoints: {
/**
* ### Endpoint
*
* POST `/sign-in/oauth2`
*
* ### API Methods
*
* **server:**
* `auth.api.signInWithOAuth2`
*
* **client:**
* `authClient.signIn.oauth2`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/sign-in#api-method-sign-in-oauth2)
*/
signInWithOAuth2: createAuthEndpoint(
"/sign-in/oauth2",
{
method: "POST",
body: z.object({
providerId: z.string().meta({
description: "The provider ID for the OAuth provider"
}),
callbackURL: z.string().meta({
description: "The URL to redirect to after sign in"
}).optional(),
errorCallbackURL: z.string().meta({
description: "The URL to redirect to if an error occurs"
}).optional(),
newUserCallbackURL: z.string().meta({
description: 'The URL to redirect to after login if the user is new. Eg: "/welcome"'
}).optional(),
disableRedirect: z.boolean().meta({
description: "Disable redirect"
}).optional(),
scopes: z.array(z.string()).meta({
description: "Scopes to be passed to the provider authorization request."
}).optional(),
requestSignUp: z.boolean().meta({
description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider. Eg: false"
}).optional()
}),
metadata: {
openapi: {
description: "Sign in with OAuth2",
responses: {
200: {
description: "Sign in with OAuth2",
content: {
"application/json": {
schema: {
type: "object",
properties: {
url: {
type: "string"
},
redirect: {
type: "boolean"
}
}
}
}
}
}
}
}
}
},
async (ctx) => {
const { providerId } = ctx.body;
const config = options.config.find(
(c) => c.providerId === providerId
);
if (!config) {
throw new APIError("BAD_REQUEST", {
message: `No config found for provider ${providerId}`
});
}
const {
discoveryUrl,
authorizationUrl,
tokenUrl,
clientId,
clientSecret,
scopes,
redirectURI,
responseType,
pkce,
prompt,
accessType,
authorizationUrlParams,
responseMode,
authentication
} = config;
let finalAuthUrl = authorizationUrl;
let finalTokenUrl = tokenUrl;
if (discoveryUrl) {
const discovery = await betterFetch(discoveryUrl, {
method: "GET",
headers: config.discoveryHeaders,
onError(context) {
ctx.context.logger.error(context.error.message, context.error, {
discoveryUrl
});
}
});
if (discovery.data) {
finalAuthUrl = discovery.data.authorization_endpoint;
finalTokenUrl = discovery.data.token_endpoint;
}
}
if (!finalAuthUrl || !finalTokenUrl) {
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION
});
}
if (authorizationUrlParams) {
const withAdditionalParams = new URL(finalAuthUrl);
for (const [paramName, paramValue] of Object.entries(
authorizationUrlParams
)) {
withAdditionalParams.searchParams.set(paramName, paramValue);
}
finalAuthUrl = withAdditionalParams.toString();
}
const { state, codeVerifier } = await generateState(ctx);
const authUrl = await createAuthorizationURL({
id: providerId,
options: {
clientId,
redirectURI
},
authorizationEndpoint: finalAuthUrl,
state,
codeVerifier: pkce ? codeVerifier : void 0,
scopes: ctx.body.scopes ? [...ctx.body.scopes, ...scopes || []] : scopes || [],
redirectURI: `${ctx.context.baseURL}/oauth2/callback/${providerId}`,
prompt,
accessType,
responseType,
responseMode,
additionalParams: authorizationUrlParams
});
return ctx.json({
url: authUrl.toString(),
redirect: !ctx.body.disableRedirect
});
}
),
oAuth2Callback: createAuthEndpoint(
"/oauth2/callback/:providerId",
{
method: "GET",
query: z.object({
code: z.string().meta({
description: "The OAuth2 code"
}).optional(),
error: z.string().meta({
description: "The error message, if any"
}).optional(),
error_description: z.string().meta({
description: "The error description, if any"
}).optional(),
state: z.string().meta({
description: "The state parameter from the OAuth2 request"
}).optional()
}),
metadata: {
client: false,
openapi: {
description: "OAuth2 callback",
responses: {
200: {
description: "OAuth2 callback",
content: {
"application/json": {
schema: {
type: "object",
properties: {
url: {
type: "string"
}
}
}
}
}
}
}
}
}
},
async (ctx) => {
const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
if (ctx.query.error || !ctx.query.code) {
throw ctx.redirect(
`${defaultErrorURL}?error=${ctx.query.error || "oAuth_code_missing"}&error_description=${ctx.query.error_description}`
);
}
const provider = options.config.find(
(p) => p.providerId === ctx.params.providerId
);
if (!provider) {
throw new APIError("BAD_REQUEST", {
message: `No config found for provider ${ctx.params.providerId}`
});
}
let tokens = void 0;
const parsedState = await parseState(ctx);
const {
callbackURL,
codeVerifier,
errorURL,
requestSignUp,
newUserURL,
link
} = parsedState;
const code = ctx.query.code;
function redirectOnError(error) {
const defaultErrorURL2 = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
let url = errorURL || defaultErrorURL2;
if (url.includes("?")) {
url = `${url}&error=${error}`;
} else {
url = `${url}?error=${error}`;
}
throw ctx.redirect(url);
}
let finalTokenUrl = provider.tokenUrl;
let finalUserInfoUrl = provider.userInfoUrl;
if (provider.discoveryUrl) {
const discovery = await betterFetch(provider.discoveryUrl, {
method: "GET",
headers: provider.discoveryHeaders
});
if (discovery.data) {
finalTokenUrl = discovery.data.token_endpoint;
finalUserInfoUrl = discovery.data.userinfo_endpoint;
}
}
try {
if (!finalTokenUrl) {
throw new APIError("BAD_REQUEST", {
message: "Invalid OAuth configuration."
});
}
tokens = await validateAuthorizationCode({
headers: provider.authorizationHeaders,
code,
codeVerifier: provider.pkce ? codeVerifier : void 0,
redirectURI: `${ctx.context.baseURL}/oauth2/callback/${provider.providerId}`,
options: {
clientId: provider.clientId,
clientSecret: provider.clientSecret,
redirectURI: provider.redirectURI
},
tokenEndpoint: finalTokenUrl,
authentication: provider.authentication,
additionalParams: provider.tokenUrlParams
});
} catch (e) {
ctx.context.logger.error(
e && typeof e === "object" && "name" in e ? e.name : "",
e
);
throw redirectOnError("oauth_code_verification_failed");
}
if (!tokens) {
throw new APIError("BAD_REQUEST", {
message: "Invalid OAuth configuration."
});
}
const userInfo = await async function handleUserInfo() {
const userInfo2 = provider.getUserInfo ? await provider.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
if (!userInfo2) {
throw redirectOnError("user_info_is_missing");
}
const mapUser = provider.mapProfileToUser ? await provider.mapProfileToUser(userInfo2) : userInfo2;
const email = mapUser.email ? mapUser.email.toLowerCase() : userInfo2.email?.toLowerCase();
if (!email) {
ctx.context.logger.error("Unable to get user info", userInfo2);
throw redirectOnError("email_is_missing");
}
const id = mapUser.id ? String(mapUser.id) : String(userInfo2.id);
const name = mapUser.name ? mapUser.name : userInfo2.name;
if (!name) {
ctx.context.logger.error("Unable to get user info", userInfo2);
throw redirectOnError("name_is_missing");
}
return {
...userInfo2,
...mapUser,
email,
id,
name
};
}();
if (link) {
if (ctx.context.options.account?.accountLinking?.allowDifferentEmails !== true && link.email !== userInfo.email) {
return redirectOnError("email_doesn't_match");
}
const existingAccount = await ctx.context.internalAdapter.findAccountByProviderId(
String(userInfo.id),
provider.providerId
);
if (existingAccount) {
if (existingAccount.userId !== link.userId) {
return redirectOnError(
"account_already_linked_to_different_user"
);
}
const updateData = Object.fromEntries(
Object.entries({
accessToken: tokens.accessToken,
idToken: tokens.idToken,
refreshToken: tokens.refreshToken,
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
scope: tokens.scopes?.join(",")
}).filter(([_, value]) => value !== void 0)
);
await ctx.context.internalAdapter.updateAccount(
existingAccount.id,
updateData
);
} else {
const newAccount = await ctx.context.internalAdapter.createAccount({
userId: link.userId,
providerId: provider.providerId,
accountId: userInfo.id,
accessToken: tokens.accessToken,
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt,
scope: tokens.scopes?.join(","),
refreshToken: tokens.refreshToken,
idToken: tokens.idToken
});
if (!newAccount) {
return redirectOnError("unable_to_link_account");
}
}
let toRedirectTo2;
try {
const url = callbackURL;
toRedirectTo2 = url.toString();
} catch {
toRedirectTo2 = callbackURL;
}
throw ctx.redirect(toRedirectTo2);
}
const result = await handleOAuthUserInfo(ctx, {
userInfo,
account: {
providerId: provider.providerId,
accountId: userInfo.id,
...tokens,
scope: tokens.scopes?.join(",")
},
callbackURL,
disableSignUp: provider.disableImplicitSignUp && !requestSignUp || provider.disableSignUp,
overrideUserInfo: provider.overrideUserInfo
});
if (result.error) {
return redirectOnError(result.error.split(" ").join("_"));
}
const { session, user } = result.data;
await setSessionCookie(ctx, {
session,
user
});
let toRedirectTo;
try {
const url = result.isRegister ? newUserURL || callbackURL : callbackURL;
toRedirectTo = url.toString();
} catch {
toRedirectTo = result.isRegister ? newUserURL || callbackURL : callbackURL;
}
throw ctx.redirect(toRedirectTo);
}
),
/**
* ### Endpoint
*
* POST `/oauth2/link`
*
* ### API Methods
*
* **server:**
* `auth.api.oAuth2LinkAccount`
*
* **client:**
* `authClient.oauth2.link`
*
* @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/generic-oauth#api-method-oauth2-link)
*/
oAuth2LinkAccount: createAuthEndpoint(
"/oauth2/link",
{
method: "POST",
body: z.object({
providerId: z.string(),
/**
* Callback URL to redirect to after the user has signed in.
*/
callbackURL: z.string(),
/**
* Additional scopes to request when linking the account.
* This is useful for requesting additional permissions when
* linking a social account compared to the initial authentication.
*/
scopes: z.array(z.string()).meta({
description: "Additional scopes to request when linking the account"
}).optional(),
/**
* The URL to redirect to if there is an error during the link process.
*/
errorCallbackURL: z.string().meta({
description: "The URL to redirect to if there is an error during the link process"
}).optional()
}),
use: [sessionMiddleware],
metadata: {
openapi: {
description: "Link an OAuth2 account to the current user session",
responses: {
"200": {
description: "Authorization URL generated successfully for linking an OAuth2 account",
content: {
"application/json": {
schema: {
type: "object",
properties: {
url: {
type: "string",
format: "uri",
description: "The authorization URL to redirect the user to for linking the OAuth2 account"
},
redirect: {
type: "boolean",
description: "Indicates that the client should redirect to the provided URL",
enum: [true]
}
},
required: ["url", "redirect"]
}
}
}
}
}
}
}
},
async (c) => {
const session = c.context.session;
const provider = options.config.find(
(p) => p.providerId === c.body.providerId
);
if (!provider) {
throw new APIError("NOT_FOUND", {
message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND
});
}
const {
providerId,
clientId,
clientSecret,
redirectURI,
authorizationUrl,
discoveryUrl,
pkce,
scopes,
prompt,
accessType,
authorizationUrlParams
} = provider;
let finalAuthUrl = authorizationUrl;
if (!finalAuthUrl) {
if (!discoveryUrl) {
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION
});
}
const discovery = await betterFetch(discoveryUrl, {
method: "GET",
headers: provider.discoveryHeaders,
onError(context) {
c.context.logger.error(context.error.message, context.error, {
discoveryUrl
});
}
});
if (discovery.data) {
finalAuthUrl = discovery.data.authorization_endpoint;
}
}
if (!finalAuthUrl) {
throw new APIError("BAD_REQUEST", {
message: ERROR_CODES.INVALID_OAUTH_CONFIGURATION
});
}
const state = await generateState(c, {
userId: session.user.id,
email: session.user.email
});
const url = await createAuthorizationURL({
id: providerId,
options: {
clientId,
redirectURI: redirectURI || `${c.context.baseURL}/oauth2/callback/${providerId}`
},
authorizationEndpoint: finalAuthUrl,
state: state.state,
codeVerifier: pkce ? state.codeVerifier : void 0,
scopes: c.body.scopes || scopes || [],
redirectURI: redirectURI || `${c.context.baseURL}/oauth2/callback/${providerId}`,
prompt,
accessType,
additionalParams: authorizationUrlParams
});
return c.json({
url: url.toString(),
redirect: true
});
}
)
},
$ERROR_CODES: ERROR_CODES
};
};
export { genericOAuth };