better-auth
Version:
The most comprehensive authentication library for TypeScript.
1,590 lines (1,576 loc) • 211 kB
JavaScript
import * as z from 'zod/v4';
import { c as createAuthMiddleware, a as createAuthEndpoint, g as getSessionFromCtx, B as BASE_ERROR_CODES, s as sessionMiddleware, f as freshSessionMiddleware } from './better-auth.DV5EHeYG.mjs';
import { APIError, toResponse } from 'better-call';
import { g as getDate } from './better-auth.CW6D9eSx.mjs';
import { createHash } from '@better-auth/utils/hash';
import { base64Url, base64 } from '@better-auth/utils/base64';
import { signJWT, symmetricEncrypt, symmetricDecrypt } from '../crypto/index.mjs';
import { betterFetch } from '@better-fetch/fetch';
import { jwtVerify, decodeJwt, decodeProtectedHeader, importJWK, createRemoteJWKSet } from 'jose';
import '@noble/ciphers/chacha.js';
import '@noble/ciphers/utils.js';
import '@noble/hashes/scrypt.js';
import '@better-auth/utils';
import '@better-auth/utils/hex';
import '@noble/hashes/utils.js';
import { g as generateRandomString } from './better-auth.B4Qoxdgc.mjs';
import { g as getOrigin, b as getHost, c as getProtocol } from './better-auth.CuS_eDdK.mjs';
import { s as setSessionCookie, d as deleteSessionCookie } from './better-auth.UfVWArIB.mjs';
import { JWTExpired } from 'jose/errors';
import '@better-auth/utils/random';
import { a as logger, s as shouldPublishLog } from './better-auth.BjBlybv-.mjs';
import { s as safeJSONParse } from './better-auth.BZZKN1g7.mjs';
import { g as generateId } from './better-auth.BUPPRXfK.mjs';
import { h as parseUserInput } from './better-auth.Dcv8PS7T.mjs';
import { b as isDevelopment } from './better-auth.CMQ3rA-I.mjs';
import '@better-auth/utils/hmac';
import '@better-auth/utils/binary';
import { createDefu } from 'defu';
import { B as BetterAuthError } from './better-auth.DdzSJf-n.mjs';
function escapeRegExpChar(char) {
if (char === "-" || char === "^" || char === "$" || char === "+" || char === "." || char === "(" || char === ")" || char === "|" || char === "[" || char === "]" || char === "{" || char === "}" || char === "*" || char === "?" || char === "\\") {
return `\\${char}`;
} else {
return char;
}
}
function escapeRegExpString(str) {
let result = "";
for (let i = 0; i < str.length; i++) {
result += escapeRegExpChar(str[i]);
}
return result;
}
function transform(pattern, separator = true) {
if (Array.isArray(pattern)) {
let regExpPatterns = pattern.map((p) => `^${transform(p, separator)}$`);
return `(?:${regExpPatterns.join("|")})`;
}
let separatorSplitter = "";
let separatorMatcher = "";
let wildcard = ".";
if (separator === true) {
separatorSplitter = "/";
separatorMatcher = "[/\\\\]";
wildcard = "[^/\\\\]";
} else if (separator) {
separatorSplitter = separator;
separatorMatcher = escapeRegExpString(separatorSplitter);
if (separatorMatcher.length > 1) {
separatorMatcher = `(?:${separatorMatcher})`;
wildcard = `((?!${separatorMatcher}).)`;
} else {
wildcard = `[^${separatorMatcher}]`;
}
}
let requiredSeparator = separator ? `${separatorMatcher}+?` : "";
let optionalSeparator = separator ? `${separatorMatcher}*?` : "";
let segments = separator ? pattern.split(separatorSplitter) : [pattern];
let result = "";
for (let s = 0; s < segments.length; s++) {
let segment = segments[s];
let nextSegment = segments[s + 1];
let currentSeparator = "";
if (!segment && s > 0) {
continue;
}
if (separator) {
if (s === segments.length - 1) {
currentSeparator = optionalSeparator;
} else if (nextSegment !== "**") {
currentSeparator = requiredSeparator;
} else {
currentSeparator = "";
}
}
if (separator && segment === "**") {
if (currentSeparator) {
result += s === 0 ? "" : currentSeparator;
result += `(?:${wildcard}*?${currentSeparator})*?`;
}
continue;
}
for (let c = 0; c < segment.length; c++) {
let char = segment[c];
if (char === "\\") {
if (c < segment.length - 1) {
result += escapeRegExpChar(segment[c + 1]);
c++;
}
} else if (char === "?") {
result += wildcard;
} else if (char === "*") {
result += `${wildcard}*?`;
} else {
result += escapeRegExpChar(char);
}
}
result += currentSeparator;
}
return result;
}
function isMatch(regexp, sample) {
if (typeof sample !== "string") {
throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
}
return regexp.test(sample);
}
function wildcardMatch(pattern, options) {
if (typeof pattern !== "string" && !Array.isArray(pattern)) {
throw new TypeError(
`The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`
);
}
if (typeof options === "string" || typeof options === "boolean") {
options = { separator: options };
}
if (arguments.length === 2 && !(typeof options === "undefined" || typeof options === "object" && options !== null && !Array.isArray(options))) {
throw new TypeError(
`The second argument must be an options object or a string/boolean separator, but ${typeof options} given`
);
}
options = options || {};
if (options.separator === "\\") {
throw new Error(
"\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead"
);
}
let regexpPattern = transform(pattern, options.separator);
let regexp = new RegExp(`^${regexpPattern}$`, options.flags);
let fn = isMatch.bind(null, regexp);
fn.options = options;
fn.pattern = pattern;
fn.regexp = regexp;
return fn;
}
const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
if (ctx.request?.method !== "POST" || !ctx.request) {
return;
}
const { body, query, context } = ctx;
const originHeader = ctx.headers?.get("origin") || ctx.headers?.get("referer") || "";
const callbackURL = body?.callbackURL || query?.callbackURL;
const redirectURL = body?.redirectTo;
const errorCallbackURL = body?.errorCallbackURL;
const newUserCallbackURL = body?.newUserCallbackURL;
const trustedOrigins = Array.isArray(context.options.trustedOrigins) ? context.trustedOrigins : [
...context.trustedOrigins,
...await context.options.trustedOrigins?.(ctx.request) || []
];
const usesCookies = ctx.headers?.has("cookie");
const matchesPattern = (url, pattern) => {
if (url.startsWith("/")) {
return false;
}
if (pattern.includes("*")) {
if (pattern.includes("://")) {
return wildcardMatch(pattern)(getOrigin(url) || url);
}
return wildcardMatch(pattern)(getHost(url));
}
const protocol = getProtocol(url);
return protocol === "http:" || protocol === "https:" || !protocol ? pattern === getOrigin(url) : url.startsWith(pattern);
};
const validateURL = (url, label) => {
if (!url) {
return;
}
const isTrustedOrigin = trustedOrigins.some(
(origin) => matchesPattern(url, origin) || url?.startsWith("/") && label !== "origin" && /^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(url)
);
if (!isTrustedOrigin) {
ctx.context.logger.error(`Invalid ${label}: ${url}`);
ctx.context.logger.info(
`If it's a valid URL, please add ${url} to trustedOrigins in your auth config
`,
`Current list of trustedOrigins: ${trustedOrigins}`
);
throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
}
};
if (usesCookies && !ctx.context.options.advanced?.disableCSRFCheck) {
validateURL(originHeader, "origin");
}
callbackURL && validateURL(callbackURL, "callbackURL");
redirectURL && validateURL(redirectURL, "redirectURL");
errorCallbackURL && validateURL(errorCallbackURL, "errorCallbackURL");
newUserCallbackURL && validateURL(newUserCallbackURL, "newUserCallbackURL");
});
const originCheck = (getValue) => createAuthMiddleware(async (ctx) => {
if (!ctx.request) {
return;
}
const { context } = ctx;
const callbackURL = getValue(ctx);
const trustedOrigins = Array.isArray(
context.options.trustedOrigins
) ? context.trustedOrigins : [
...context.trustedOrigins,
...await context.options.trustedOrigins?.(ctx.request) || []
];
const matchesPattern = (url, pattern) => {
if (url.startsWith("/")) {
return false;
}
if (pattern.includes("*")) {
if (pattern.includes("://")) {
return wildcardMatch(pattern)(getOrigin(url) || url);
}
return wildcardMatch(pattern)(getHost(url));
}
const protocol = getProtocol(url);
return protocol === "http:" || protocol === "https:" || !protocol ? pattern === getOrigin(url) : url.startsWith(pattern);
};
const validateURL = (url, label) => {
if (!url) {
return;
}
const isTrustedOrigin = trustedOrigins.some(
(origin) => matchesPattern(url, origin) || url?.startsWith("/") && label !== "origin" && /^\/(?!\/|\\|%2f|%5c)[\w\-.\+/@]*(?:\?[\w\-.\+/=&%@]*)?$/.test(
url
)
);
if (!isTrustedOrigin) {
ctx.context.logger.error(`Invalid ${label}: ${url}`);
ctx.context.logger.info(
`If it's a valid URL, please add ${url} to trustedOrigins in your auth config
`,
`Current list of trustedOrigins: ${trustedOrigins}`
);
throw new APIError("FORBIDDEN", { message: `Invalid ${label}` });
}
};
const callbacks = Array.isArray(callbackURL) ? callbackURL : [callbackURL];
for (const url of callbacks) {
validateURL(url, "callbackURL");
}
});
async function createEmailVerificationToken(secret, email, updateTo, expiresIn = 3600) {
const token = await signJWT(
{
email: email.toLowerCase(),
updateTo
},
secret,
expiresIn
);
return token;
}
async function sendVerificationEmailFn(ctx, user) {
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
ctx.context.logger.error("Verification email isn't enabled.");
throw new APIError("BAD_REQUEST", {
message: "Verification email isn't enabled"
});
}
const token = await createEmailVerificationToken(
ctx.context.secret,
user.email,
void 0,
ctx.context.options.emailVerification?.expiresIn
);
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || "/"}`;
await ctx.context.options.emailVerification.sendVerificationEmail(
{
user,
url,
token
},
ctx.request
);
}
const sendVerificationEmail = createAuthEndpoint(
"/send-verification-email",
{
method: "POST",
body: z.object({
email: z.email().meta({
description: "The email to send the verification email to"
}),
callbackURL: z.string().meta({
description: "The URL to use for email verification callback"
}).optional()
}),
metadata: {
openapi: {
description: "Send a verification email to the user",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
email: {
type: "string",
description: "The email to send the verification email to",
example: "user@example.com"
},
callbackURL: {
type: "string",
description: "The URL to use for email verification callback",
example: "https://example.com/callback",
nullable: true
}
},
required: ["email"]
}
}
}
},
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
status: {
type: "boolean",
description: "Indicates if the email was sent successfully",
example: true
}
}
}
}
}
},
"400": {
description: "Bad Request",
content: {
"application/json": {
schema: {
type: "object",
properties: {
message: {
type: "string",
description: "Error message",
example: "Verification email isn't enabled"
}
}
}
}
}
}
}
}
}
},
async (ctx) => {
if (!ctx.context.options.emailVerification?.sendVerificationEmail) {
ctx.context.logger.error("Verification email isn't enabled.");
throw new APIError("BAD_REQUEST", {
message: "Verification email isn't enabled"
});
}
const { email } = ctx.body;
const session = await getSessionFromCtx(ctx);
if (!session) {
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
return ctx.json({
status: true
});
}
await sendVerificationEmailFn(ctx, user.user);
return ctx.json({
status: true
});
}
if (session?.user.emailVerified) {
throw new APIError("BAD_REQUEST", {
message: "You can only send a verification email to an unverified email"
});
}
if (session?.user.email !== email) {
throw new APIError("BAD_REQUEST", {
message: "You can only send a verification email to your own email"
});
}
await sendVerificationEmailFn(ctx, session.user);
return ctx.json({
status: true
});
}
);
const verifyEmail = createAuthEndpoint(
"/verify-email",
{
method: "GET",
query: z.object({
token: z.string().meta({
description: "The token to verify the email"
}),
callbackURL: z.string().meta({
description: "The URL to redirect to after email verification"
}).optional()
}),
use: [originCheck((ctx) => ctx.query.callbackURL)],
metadata: {
openapi: {
description: "Verify the email of the user",
parameters: [
{
name: "token",
in: "query",
description: "The token to verify the email",
required: true,
schema: {
type: "string"
}
},
{
name: "callbackURL",
in: "query",
description: "The URL to redirect to after email verification",
required: false,
schema: {
type: "string"
}
}
],
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
user: {
type: "object",
properties: {
id: {
type: "string",
description: "User ID"
},
email: {
type: "string",
description: "User email"
},
name: {
type: "string",
description: "User name"
},
image: {
type: "string",
description: "User image URL"
},
emailVerified: {
type: "boolean",
description: "Indicates if the user email is verified"
},
createdAt: {
type: "string",
description: "User creation date"
},
updatedAt: {
type: "string",
description: "User update date"
}
},
required: [
"id",
"email",
"name",
"image",
"emailVerified",
"createdAt",
"updatedAt"
]
},
status: {
type: "boolean",
description: "Indicates if the email was verified successfully"
}
},
required: ["user", "status"]
}
}
}
}
}
}
}
},
async (ctx) => {
function redirectOnError(error) {
if (ctx.query.callbackURL) {
if (ctx.query.callbackURL.includes("?")) {
throw ctx.redirect(`${ctx.query.callbackURL}&error=${error}`);
}
throw ctx.redirect(`${ctx.query.callbackURL}?error=${error}`);
}
throw new APIError("UNAUTHORIZED", {
message: error
});
}
const { token } = ctx.query;
let jwt;
try {
jwt = await jwtVerify(
token,
new TextEncoder().encode(ctx.context.secret),
{
algorithms: ["HS256"]
}
);
} catch (e) {
if (e instanceof JWTExpired) {
return redirectOnError("token_expired");
}
return redirectOnError("invalid_token");
}
const schema = z.object({
email: z.string().email(),
updateTo: z.string().optional()
});
const parsed = schema.parse(jwt.payload);
const user = await ctx.context.internalAdapter.findUserByEmail(
parsed.email
);
if (!user) {
return redirectOnError("user_not_found");
}
if (parsed.updateTo) {
const session = await getSessionFromCtx(ctx);
if (!session) {
if (ctx.query.callbackURL) {
throw ctx.redirect(`${ctx.query.callbackURL}?error=unauthorized`);
}
return redirectOnError("unauthorized");
}
if (session.user.email !== parsed.email) {
if (ctx.query.callbackURL) {
throw ctx.redirect(`${ctx.query.callbackURL}?error=unauthorized`);
}
return redirectOnError("unauthorized");
}
const updatedUser2 = await ctx.context.internalAdapter.updateUserByEmail(
parsed.email,
{
email: parsed.updateTo,
emailVerified: false
},
ctx
);
const newToken = await createEmailVerificationToken(
ctx.context.secret,
parsed.updateTo
);
await ctx.context.options.emailVerification?.sendVerificationEmail?.(
{
user: updatedUser2,
url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${ctx.query.callbackURL || "/"}`,
token: newToken
},
ctx.request
);
await setSessionCookie(ctx, {
session: session.session,
user: {
...session.user,
email: parsed.updateTo,
emailVerified: false
}
});
if (ctx.query.callbackURL) {
throw ctx.redirect(ctx.query.callbackURL);
}
return ctx.json({
status: true,
user: {
id: updatedUser2.id,
email: updatedUser2.email,
name: updatedUser2.name,
image: updatedUser2.image,
emailVerified: updatedUser2.emailVerified,
createdAt: updatedUser2.createdAt,
updatedAt: updatedUser2.updatedAt
}
});
}
if (ctx.context.options.emailVerification?.onEmailVerification) {
await ctx.context.options.emailVerification.onEmailVerification(
user.user,
ctx.request
);
}
const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(
parsed.email,
{
emailVerified: true
},
ctx
);
if (ctx.context.options.emailVerification?.afterEmailVerification) {
await ctx.context.options.emailVerification.afterEmailVerification(
updatedUser,
ctx.request
);
}
if (ctx.context.options.emailVerification?.autoSignInAfterVerification) {
const currentSession = await getSessionFromCtx(ctx);
if (!currentSession || currentSession.user.email !== parsed.email) {
const session = await ctx.context.internalAdapter.createSession(
user.user.id,
ctx
);
if (!session) {
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Failed to create session"
});
}
await setSessionCookie(ctx, {
session,
user: {
...user.user,
emailVerified: true
}
});
} else {
await setSessionCookie(ctx, {
session: currentSession.session,
user: {
...currentSession.user,
emailVerified: true
}
});
}
}
if (ctx.query.callbackURL) {
throw ctx.redirect(ctx.query.callbackURL);
}
return ctx.json({
status: true,
user: null
});
}
);
const HIDE_METADATA = {
isAction: false
};
async function generateState(c, link) {
const callbackURL = c.body?.callbackURL || c.context.options.baseURL;
if (!callbackURL) {
throw new APIError("BAD_REQUEST", {
message: "callbackURL is required"
});
}
const codeVerifier = generateRandomString(128);
const state = generateRandomString(32);
const data = JSON.stringify({
callbackURL,
codeVerifier,
errorURL: c.body?.errorCallbackURL,
newUserURL: c.body?.newUserCallbackURL,
link,
/**
* This is the actual expiry time of the state
*/
expiresAt: Date.now() + 10 * 60 * 1e3,
requestSignUp: c.body?.requestSignUp
});
const expiresAt = /* @__PURE__ */ new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 10);
const verification = await c.context.internalAdapter.createVerificationValue(
{
value: data,
identifier: state,
expiresAt
},
c
);
if (!verification) {
c.context.logger.error(
"Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database"
);
throw new APIError("INTERNAL_SERVER_ERROR", {
message: "Unable to create verification"
});
}
return {
state: verification.identifier,
codeVerifier
};
}
async function parseState(c) {
const state = c.query.state || c.body.state;
const data = await c.context.internalAdapter.findVerificationValue(state);
if (!data) {
c.context.logger.error("State Mismatch. Verification not found", {
state
});
const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
throw c.redirect(`${errorURL}?error=please_restart_the_process`);
}
const parsedData = z.object({
callbackURL: z.string(),
codeVerifier: z.string(),
errorURL: z.string().optional(),
newUserURL: z.string().optional(),
expiresAt: z.number(),
link: z.object({
email: z.string(),
userId: z.coerce.string()
}).optional(),
requestSignUp: z.boolean().optional()
}).parse(JSON.parse(data.value));
if (!parsedData.errorURL) {
parsedData.errorURL = `${c.context.baseURL}/error`;
}
if (parsedData.expiresAt < Date.now()) {
await c.context.internalAdapter.deleteVerificationValue(data.id);
const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
throw c.redirect(`${errorURL}?error=please_restart_the_process`);
}
await c.context.internalAdapter.deleteVerificationValue(data.id);
return parsedData;
}
async function generateCodeChallenge(codeVerifier) {
const codeChallengeBytes = await createHash("SHA-256").digest(codeVerifier);
return base64Url.encode(new Uint8Array(codeChallengeBytes), {
padding: false
});
}
function getOAuth2Tokens(data) {
return {
tokenType: data.token_type,
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessTokenExpiresAt: data.expires_in ? getDate(data.expires_in, "sec") : void 0,
refreshTokenExpiresAt: data.refresh_token_expires_in ? getDate(data.refresh_token_expires_in, "sec") : void 0,
scopes: data?.scope ? typeof data.scope === "string" ? data.scope.split(" ") : data.scope : [],
idToken: data.id_token
};
}
const encodeOAuthParameter = (value) => encodeURIComponent(value).replace(/%20/g, "+");
function decryptOAuthToken(token, ctx) {
if (!token) return token;
if (ctx.options.account?.encryptOAuthTokens) {
return symmetricDecrypt({
key: ctx.secret,
data: token
});
}
return token;
}
function setTokenUtil(token, ctx) {
if (ctx.options.account?.encryptOAuthTokens && token) {
return symmetricEncrypt({
key: ctx.secret,
data: token
});
}
return token;
}
async function handleOAuthUserInfo(c, {
userInfo,
account,
callbackURL,
disableSignUp,
overrideUserInfo
}) {
const dbUser = await c.context.internalAdapter.findOAuthUser(
userInfo.email.toLowerCase(),
account.accountId,
account.providerId
).catch((e) => {
logger.error(
"Better auth was unable to query your database.\nError: ",
e
);
const errorURL = c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
throw c.redirect(`${errorURL}?error=internal_server_error`);
});
let user = dbUser?.user;
let isRegister = !user;
if (dbUser) {
const hasBeenLinked = dbUser.accounts.find(
(a) => a.providerId === account.providerId && a.accountId === account.accountId
);
if (!hasBeenLinked) {
const trustedProviders = c.context.options.account?.accountLinking?.trustedProviders;
const isTrustedProvider = trustedProviders?.includes(
account.providerId
);
if (!isTrustedProvider && !userInfo.emailVerified || c.context.options.account?.accountLinking?.enabled === false) {
if (isDevelopment) {
logger.warn(
`User already exist but account isn't linked to ${account.providerId}. To read more about how account linking works in Better Auth see https://www.better-auth.com/docs/concepts/users-accounts#account-linking.`
);
}
return {
error: "account not linked",
data: null
};
}
try {
await c.context.internalAdapter.linkAccount(
{
providerId: account.providerId,
accountId: userInfo.id.toString(),
userId: dbUser.user.id,
accessToken: await setTokenUtil(account.accessToken, c.context),
refreshToken: await setTokenUtil(account.refreshToken, c.context),
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope
},
c
);
} catch (e) {
logger.error("Unable to link account", e);
return {
error: "unable to link account",
data: null
};
}
} else {
if (c.context.options.account?.updateAccountOnSignIn !== false) {
const updateData = Object.fromEntries(
Object.entries({
idToken: account.idToken,
accessToken: await setTokenUtil(account.accessToken, c.context),
refreshToken: await setTokenUtil(account.refreshToken, c.context),
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope
}).filter(([_, value]) => value !== void 0)
);
if (Object.keys(updateData).length > 0) {
await c.context.internalAdapter.updateAccount(
hasBeenLinked.id,
updateData,
c
);
}
}
}
if (overrideUserInfo) {
const { id: _, ...restUserInfo } = userInfo;
await c.context.internalAdapter.updateUser(dbUser.user.id, {
...restUserInfo,
email: userInfo.email.toLowerCase(),
emailVerified: userInfo.email.toLowerCase() === dbUser.user.email ? dbUser.user.emailVerified || userInfo.emailVerified : userInfo.emailVerified
});
}
} else {
if (disableSignUp) {
return {
error: "signup disabled",
data: null,
isRegister: false
};
}
try {
const { id: _, ...restUserInfo } = userInfo;
user = await c.context.internalAdapter.createOAuthUser(
{
...restUserInfo,
email: userInfo.email.toLowerCase()
},
{
accessToken: await setTokenUtil(account.accessToken, c.context),
refreshToken: await setTokenUtil(account.refreshToken, c.context),
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope,
providerId: account.providerId,
accountId: userInfo.id.toString()
},
c
).then((res) => res?.user);
if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp) {
const token = await createEmailVerificationToken(
c.context.secret,
user.email,
void 0,
c.context.options.emailVerification?.expiresIn
);
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
await c.context.options.emailVerification?.sendVerificationEmail?.(
{
user,
url,
token
},
c.request
);
}
} catch (e) {
logger.error(e);
if (e instanceof APIError) {
return {
error: e.message,
data: null,
isRegister: false
};
}
return {
error: "unable to create user",
data: null,
isRegister: false
};
}
}
if (!user) {
return {
error: "unable to create user",
data: null,
isRegister: false
};
}
const session = await c.context.internalAdapter.createSession(user.id, c);
if (!session) {
return {
error: "unable to create session",
data: null,
isRegister: false
};
}
return {
data: {
session,
user
},
error: null,
isRegister
};
}
async function createAuthorizationURL({
id,
options,
authorizationEndpoint,
state,
codeVerifier,
scopes,
claims,
redirectURI,
duration,
prompt,
accessType,
responseType,
display,
loginHint,
hd,
responseMode,
additionalParams,
scopeJoiner
}) {
const url = new URL(authorizationEndpoint);
url.searchParams.set("response_type", responseType || "code");
url.searchParams.set("client_id", options.clientId);
url.searchParams.set("state", state);
url.searchParams.set("scope", scopes.join(scopeJoiner || " "));
url.searchParams.set("redirect_uri", options.redirectURI || redirectURI);
duration && url.searchParams.set("duration", duration);
display && url.searchParams.set("display", display);
loginHint && url.searchParams.set("login_hint", loginHint);
prompt && url.searchParams.set("prompt", prompt);
hd && url.searchParams.set("hd", hd);
accessType && url.searchParams.set("access_type", accessType);
responseMode && url.searchParams.set("response_mode", responseMode);
if (codeVerifier) {
const codeChallenge = await generateCodeChallenge(codeVerifier);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("code_challenge", codeChallenge);
}
if (claims) {
const claimsObj = claims.reduce(
(acc, claim) => {
acc[claim] = null;
return acc;
},
{}
);
url.searchParams.set(
"claims",
JSON.stringify({
id_token: { email: null, email_verified: null, ...claimsObj }
})
);
}
if (additionalParams) {
Object.entries(additionalParams).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
return url;
}
function createAuthorizationCodeRequest({
code,
codeVerifier,
redirectURI,
options,
authentication,
deviceId,
headers,
additionalParams = {},
resource
}) {
const body = new URLSearchParams();
const requestHeaders = {
"content-type": "application/x-www-form-urlencoded",
accept: "application/json",
"user-agent": "better-auth",
...headers
};
body.set("grant_type", "authorization_code");
body.set("code", code);
codeVerifier && body.set("code_verifier", codeVerifier);
options.clientKey && body.set("client_key", options.clientKey);
deviceId && body.set("device_id", deviceId);
body.set("redirect_uri", options.redirectURI || redirectURI);
if (resource) {
if (typeof resource === "string") {
body.append("resource", resource);
} else {
for (const _resource of resource) {
body.append("resource", _resource);
}
}
}
if (authentication === "basic") {
const encodedCredentials = base64.encode(
`${options.clientId}:${options.clientSecret ?? ""}`
);
requestHeaders["authorization"] = `Basic ${encodedCredentials}`;
} else {
options.clientId && body.set("client_id", options.clientId);
if (options.clientSecret) {
body.set("client_secret", options.clientSecret);
}
}
for (const [key, value] of Object.entries(additionalParams)) {
if (!body.has(key)) body.append(key, value);
}
return {
body,
headers: requestHeaders
};
}
async function validateAuthorizationCode({
code,
codeVerifier,
redirectURI,
options,
tokenEndpoint,
authentication,
deviceId,
headers,
additionalParams = {},
resource
}) {
const { body, headers: requestHeaders } = createAuthorizationCodeRequest({
code,
codeVerifier,
redirectURI,
options,
authentication,
deviceId,
headers,
additionalParams,
resource
});
const { data, error } = await betterFetch(tokenEndpoint, {
method: "POST",
body,
headers: requestHeaders
});
if (error) {
throw error;
}
const tokens = getOAuth2Tokens(data);
return tokens;
}
async function validateToken(token, jwksEndpoint) {
const { data, error } = await betterFetch(jwksEndpoint, {
method: "GET",
headers: {
accept: "application/json",
"user-agent": "better-auth"
}
});
if (error) {
throw error;
}
const keys = data["keys"];
const header = JSON.parse(atob(token.split(".")[0]));
const key = keys.find((key2) => key2.kid === header.kid);
if (!key) {
throw new Error("Key not found");
}
const verified = await jwtVerify(token, key);
return verified;
}
function createRefreshAccessTokenRequest({
refreshToken,
options,
authentication,
extraParams,
resource
}) {
const body = new URLSearchParams();
const headers = {
"content-type": "application/x-www-form-urlencoded",
accept: "application/json"
};
body.set("grant_type", "refresh_token");
body.set("refresh_token", refreshToken);
if (authentication === "basic") {
if (options.clientId) {
headers["authorization"] = "Basic " + base64.encode(`${options.clientId}:${options.clientSecret ?? ""}`);
} else {
headers["authorization"] = "Basic " + base64.encode(`:${options.clientSecret ?? ""}`);
}
} else {
options.clientId && body.set("client_id", options.clientId);
if (options.clientSecret) {
body.set("client_secret", options.clientSecret);
}
}
if (resource) {
if (typeof resource === "string") {
body.append("resource", resource);
} else {
for (const _resource of resource) {
body.append("resource", _resource);
}
}
}
if (extraParams) {
for (const [key, value] of Object.entries(extraParams)) {
body.set(key, value);
}
}
return {
body,
headers
};
}
async function refreshAccessToken({
refreshToken,
options,
tokenEndpoint,
authentication,
extraParams
}) {
const { body, headers } = createRefreshAccessTokenRequest({
refreshToken,
options,
authentication,
extraParams
});
const { data, error } = await betterFetch(tokenEndpoint, {
method: "POST",
body,
headers
});
if (error) {
throw error;
}
const tokens = {
accessToken: data.access_token,
refreshToken: data.refresh_token,
tokenType: data.token_type,
scopes: data.scope?.split(" "),
idToken: data.id_token
};
if (data.expires_in) {
const now = /* @__PURE__ */ new Date();
tokens.accessTokenExpiresAt = new Date(
now.getTime() + data.expires_in * 1e3
);
}
return tokens;
}
const apple = (options) => {
const tokenEndpoint = "https://appleid.apple.com/auth/token";
return {
id: "apple",
name: "Apple",
async createAuthorizationURL({ state, scopes, redirectURI }) {
const _scope = options.disableDefaultScope ? [] : ["email", "name"];
options.scope && _scope.push(...options.scope);
scopes && _scope.push(...scopes);
const url = await createAuthorizationURL({
id: "apple",
options,
authorizationEndpoint: "https://appleid.apple.com/auth/authorize",
scopes: _scope,
state,
redirectURI,
responseMode: "form_post",
responseType: "code id_token"
});
return url;
},
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
return validateAuthorizationCode({
code,
codeVerifier,
redirectURI,
options,
tokenEndpoint
});
},
async verifyIdToken(token, nonce) {
if (options.disableIdTokenSignIn) {
return false;
}
if (options.verifyIdToken) {
return options.verifyIdToken(token, nonce);
}
const decodedHeader = decodeProtectedHeader(token);
const { kid, alg: jwtAlg } = decodedHeader;
if (!kid || !jwtAlg) return false;
const publicKey = await getApplePublicKey(kid);
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
algorithms: [jwtAlg],
issuer: "https://appleid.apple.com",
audience: options.audience && options.audience.length ? options.audience : options.appBundleIdentifier ? options.appBundleIdentifier : options.clientId,
maxTokenAge: "1h"
});
["email_verified", "is_private_email"].forEach((field) => {
if (jwtClaims[field] !== void 0) {
jwtClaims[field] = Boolean(jwtClaims[field]);
}
});
if (nonce && jwtClaims.nonce !== nonce) {
return false;
}
return !!jwtClaims;
},
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
return refreshAccessToken({
refreshToken,
options: {
clientId: options.clientId,
clientKey: options.clientKey,
clientSecret: options.clientSecret
},
tokenEndpoint: "https://appleid.apple.com/auth/token"
});
},
async getUserInfo(token) {
if (options.getUserInfo) {
return options.getUserInfo(token);
}
if (!token.idToken) {
return null;
}
const profile = decodeJwt(token.idToken);
if (!profile) {
return null;
}
const name = token.user ? `${token.user.name?.firstName} ${token.user.name?.lastName}` : profile.name || profile.email;
const emailVerified = typeof profile.email_verified === "boolean" ? profile.email_verified : profile.email_verified === "true";
const enrichedProfile = {
...profile,
name
};
const userMap = await options.mapProfileToUser?.(enrichedProfile);
return {
user: {
id: profile.sub,
name: enrichedProfile.name,
emailVerified,
email: profile.email,
...userMap
},
data: enrichedProfile
};
},
options
};
};
const getApplePublicKey = async (kid) => {
const APPLE_BASE_URL = "https://appleid.apple.com";
const JWKS_APPLE_URI = "/auth/keys";
const { data } = await betterFetch(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`);
if (!data?.keys) {
throw new APIError("BAD_REQUEST", {
message: "Keys not found"
});
}
const jwk = data.keys.find((key) => key.kid === kid);
if (!jwk) {
throw new Error(`JWK with kid ${kid} not found`);
}
return await importJWK(jwk, jwk.alg);
};
const atlassian = (options) => {
return {
id: "atlassian",
name: "Atlassian",
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
if (!options.clientId || !options.clientSecret) {
logger.error("Client Id and Secret are required for Atlassian");
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
}
if (!codeVerifier) {
throw new BetterAuthError("codeVerifier is required for Atlassian");
}
const _scopes = options.disableDefaultScope ? [] : ["read:jira-user", "offline_access"];
options.scope && _scopes.push(...options.scope);
scopes && _scopes.push(...scopes);
return createAuthorizationURL({
id: "atlassian",
options,
authorizationEndpoint: "https://auth.atlassian.com/authorize",
scopes: _scopes,
state,
codeVerifier,
redirectURI,
additionalParams: {
audience: "api.atlassian.com"
},
prompt: options.prompt
});
},
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
return validateAuthorizationCode({
code,
codeVerifier,
redirectURI,
options,
tokenEndpoint: "https://auth.atlassian.com/oauth/token"
});
},
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
return refreshAccessToken({
refreshToken,
options: {
clientId: options.clientId,
clientSecret: options.clientSecret
},
tokenEndpoint: "https://auth.atlassian.com/oauth/token"
});
},
async getUserInfo(token) {
if (options.getUserInfo) {
return options.getUserInfo(token);
}
if (!token.accessToken) {
return null;
}
try {
const { data: profile } = await betterFetch("https://api.atlassian.com/me", {
headers: { Authorization: `Bearer ${token.accessToken}` }
});
if (!profile) return null;
const userMap = await options.mapProfileToUser?.(profile);
return {
user: {
id: profile.account_id,
name: profile.name,
email: profile.email,
image: profile.picture,
emailVerified: false,
...userMap
},
data: profile
};
} catch (error) {
logger.error("Failed to fetch user info from Figma:", error);
return null;
}
},
options
};
};
const cognito = (options) => {
if (!options.domain || !options.region || !options.userPoolId) {
logger.error(
"Domain, region and userPoolId are required for Amazon Cognito. Make sure to provide them in the options."
);
throw new BetterAuthError("DOMAIN_AND_REGION_REQUIRED");
}
const cleanDomain = options.domain.replace(/^https?:\/\//, "");
const authorizationEndpoint = `https://${cleanDomain}/oauth2/authorize`;
const tokenEndpoint = `https://${cleanDomain}/oauth2/token`;
const userInfoEndpoint = `https://${cleanDomain}/oauth2/userinfo`;
return {
id: "cognito",
name: "Cognito",
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
if (!options.clientId) {
logger.error(
"ClientId is required for Amazon Cognito. Make sure to provide them in the options."
);
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
}
if (options.requireClientSecret && !options.clientSecret) {
logger.error(
"Client Secret is required when requireClientSecret is true. Make sure to provide it in the options."
);
throw new BetterAuthError("CLIENT_SECRET_REQUIRED");
}
const _scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email"];
options.scope && _scopes.push(...options.scope);
scopes && _scopes.push(...scopes);
const url = await createAuthorizationURL({
id: "cognito",
options: {
...options
},
authorizationEndpoint,
scopes: _scopes,
state,
codeVerifier,
redirectURI,
prompt: options.prompt
});
return url;
},
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
return validateAuthorizationCode({
code,
codeVerifier,
redirectURI,
options,
tokenEndpoint
});
},
refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => {
return refreshAccessToken({
refreshToken,
options: {
clientId: options.clientId,
clientKey: options.clientKey,
clientSecret: options.clientSecret
},
tokenEndpoint
});
},
async verifyIdToken(token, nonce) {
if (options.disableIdTokenSignIn) {
return false;
}
if (options.verifyIdToken) {
return options.verifyIdToken(token, nonce);
}
try {
const decodedHeader = decodeProtectedHeader(token);
const { kid, alg: jwtAlg } = decodedHeader;
if (!kid || !jwtAlg) return false;
const publicKey = await getCognitoPublicKey(
kid,
options.region,
options.userPoolId
);
const expectedIssuer = `https://cognito-idp.${options.region}.amazonaws.com/${options.userPoolId}`;
const { payload: jwtClaims } = await jwtVerify(token, publicKey, {
algorithms: [jwtAlg],
issuer: expectedIssuer,
audience: options.clientId,
maxTokenAge: "1h"
});
if (nonce && jwtClaims.nonce !== nonce) {
return false;
}
return true;
} catch (error) {
logger.error("Failed to verify ID token:", error);
return false;
}
},
async getUserInfo(token) {
if (options.getUserInfo) {
return options.getUserInfo(token);
}
if (token.idToken) {
try {
const profile = decodeJwt(token.idToken);
if (!profile) {
return null;
}
const name = profile.name || profile.given_name || profile.username || profile.email;
const enrichedProfile = {
...profile,
name
};
const userMap = await options.mapProfileToUser?.(enrichedProfile);
return {
user: {
id: profile.sub,
name: enrichedProfile.name,
email: profile.email,
image: profile.picture,
emailVerified: profile.email_verified,
...userMap
},
data: enrichedProfile
};
} catch (error) {
logger.error("Failed to decode ID token:", error);
}
}
if (token.accessToken) {
try {
const { data: userInfo } = await betterFetch(
userInfoEndpoint,
{
headers: {
Authorization: `Bearer ${token.accessToken}`
}
}
);
if (userInfo) {
const userMap = await options.mapProfileToUser?.(userInfo);
return {
user: {
id: userInfo.sub,
name: userInfo.name || userInfo.given_name || userInfo.username,
email: userInfo.email,
image: userInfo.picture,
emailVerified: userInfo.email_verified,
...userMap
},
data: userInfo
};
}
} catch (error) {
logger.error("Failed to fetch user info from Cognito:", error);
}
}
return null;
},
options
};
};
const getCognitoPublicKey = async (kid, region, userPoolId) => {
const COGNITO_JWKS_URI = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`;
try {
const { data } = await betterFetch(COGNITO_JWKS_URI);
if (!data?.keys) {
throw new APIError("BAD_REQUEST", {
message: "Keys not found"
});
}
const jwk = data.keys.find((key) => key.kid === kid);
if (!jwk) {
throw new Error(`JWK with kid ${kid} not found`);
}
return await importJWK(jwk, jwk.alg);
} catch (error) {
logger.error("Failed to fetch Cognito public key:", error);
throw error;
}
};
const discord = (options) => {
return {
id: "discord",
name: "Discord",
createAuthorizationURL({ state, scopes, redirectURI }) {
const _scopes = options.disableDefaultScope ? [] : ["identify", "email"];
scopes && _scopes.push(...scopes);
options.scope && _scopes.push(...options.scope);
return new URL(
`https://discord.com/api/oauth2/authorize?scope=${_scopes.join(
"+"
)}&response_type=code&client_id=${options.clientId}&redirect_uri=${encodeURIComponent(
options.redirectURI ||