UNPKG

better-auth

Version:

The most comprehensive authentication library for TypeScript.

1,590 lines (1,576 loc) 211 kB
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 ||