UNPKG

better-auth

Version:

The most comprehensive authentication library for TypeScript.

1,595 lines (1,573 loc) 48.7 kB
'use strict'; const fetch = require('@better-fetch/fetch'); const betterCall = require('better-call'); const jose = require('jose'); const refreshAccessToken = require('../shared/better-auth.6XyKj7DG.cjs'); require('@better-auth/utils/hash'); const base64 = require('@better-auth/utils/base64'); const zod = require('zod'); require('@noble/ciphers/chacha'); require('@noble/ciphers/utils'); require('@noble/ciphers/webcrypto'); require('@noble/hashes/scrypt'); require('@better-auth/utils'); require('@better-auth/utils/hex'); require('@noble/hashes/utils'); require('../shared/better-auth.CYeOI8C-.cjs'); const index = require('../shared/better-auth.ANpbi45u.cjs'); const logger = require('../shared/better-auth.GpOOav9x.cjs'); require('@better-auth/utils/random'); require('../shared/better-auth.C1hdVENX.cjs'); 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 refreshAccessToken.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 refreshAccessToken.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 = jose.decodeProtectedHeader(token); const { kid, alg: jwtAlg } = decodedHeader; if (!kid || !jwtAlg) return false; const publicKey = await getApplePublicKey(kid); const { payload: jwtClaims } = await jose.jwtVerify(token, publicKey, { algorithms: [jwtAlg], issuer: "https://appleid.apple.com", audience: 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.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 = jose.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 userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.sub, name, emailVerified, email: profile.email, ...userMap }, data: profile }; }, options }; }; const getApplePublicKey = async (kid) => { const APPLE_BASE_URL = "https://appleid.apple.com"; const JWKS_APPLE_URI = "/auth/keys"; const { data } = await fetch.betterFetch(`${APPLE_BASE_URL}${JWKS_APPLE_URI}`); if (!data?.keys) { throw new betterCall.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 jose.importJWK(jwk, jwk.alg); }; 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 || redirectURI )}&state=${state}&prompt=${options.prompt || "none"}` ); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint: "https://discord.com/api/oauth2/token" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://discord.com/api/oauth2/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://discord.com/api/users/@me", { headers: { authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } if (profile.avatar === null) { const defaultAvatarNumber = profile.discriminator === "0" ? Number(BigInt(profile.id) >> BigInt(22)) % 6 : parseInt(profile.discriminator) % 5; profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`; } else { const format = profile.avatar.startsWith("a_") ? "gif" : "png"; profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.global_name || profile.username || "", email: profile.email, emailVerified: profile.verified, image: profile.image_url, ...userMap }, data: profile }; }, options }; }; const facebook = (options) => { return { id: "facebook", name: "Facebook", async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) { const _scopes = options.disableDefaultScope ? [] : ["email", "public_profile"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return await refreshAccessToken.createAuthorizationURL({ id: "facebook", options, authorizationEndpoint: "https://www.facebook.com/v21.0/dialog/oauth", scopes: _scopes, state, redirectURI, loginHint, additionalParams: options.configId ? { config_id: options.configId } : {} }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint: "https://graph.facebook.com/oauth/access_token" }); }, async verifyIdToken(token, nonce) { if (options.disableIdTokenSignIn) { return false; } if (options.verifyIdToken) { return options.verifyIdToken(token, nonce); } if (token.split(".").length) { try { const { payload: jwtClaims } = await jose.jwtVerify( token, jose.createRemoteJWKSet( new URL("https://www.facebook.com/.well-known/oauth/openid/jwks") ), { algorithms: ["RS256"], audience: options.clientId, issuer: "https://www.facebook.com" } ); if (nonce && jwtClaims.nonce !== nonce) { return false; } return !!jwtClaims; } catch (error) { return false; } } return true; }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://graph.facebook.com/v18.0/oauth/access_token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (token.idToken) { const profile2 = jose.decodeJwt(token.idToken); const user = { id: profile2.sub, name: profile2.name, email: profile2.email, picture: { data: { url: profile2.picture, height: 100, width: 100, is_silhouette: false } } }; const userMap2 = await options.mapProfileToUser?.({ ...user, email_verified: true }); return { user: { ...user, emailVerified: true, ...userMap2 }, data: profile2 }; } const fields = [ "id", "name", "email", "picture", ...options?.fields || [] ]; const { data: profile, error } = await fetch.betterFetch( "https://graph.facebook.com/me?fields=" + fields.join(","), { auth: { type: "Bearer", token: token.accessToken } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.name, email: profile.email, image: profile.picture.data.url, emailVerified: profile.email_verified, ...userMap }, data: profile }; }, options }; }; const github = (options) => { const tokenEndpoint = "https://github.com/login/oauth/access_token"; return { id: "github", name: "GitHub", createAuthorizationURL({ state, scopes, loginHint, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["read:user", "user:email"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return refreshAccessToken.createAuthorizationURL({ id: "github", options, authorizationEndpoint: "https://github.com/login/oauth/authorize", scopes: _scopes, state, redirectURI, loginHint, prompt: options.prompt }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://github.com/login/oauth/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://api.github.com/user", { headers: { "User-Agent": "better-auth", authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } const { data: emails } = await fetch.betterFetch("https://api.github.com/user/emails", { headers: { Authorization: `Bearer ${token.accessToken}`, "User-Agent": "better-auth" } }); if (!profile.email && emails) { profile.email = (emails.find((e) => e.primary) ?? emails[0])?.email; } const emailVerified = emails?.find((e) => e.email === profile.email)?.verified ?? false; const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id.toString(), name: profile.name || profile.login, email: profile.email, image: profile.avatar_url, emailVerified, ...userMap }, data: profile }; }, options }; }; const google = (options) => { return { id: "google", name: "Google", async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI, loginHint, display }) { if (!options.clientId || !options.clientSecret) { logger.logger.error( "Client Id and Client Secret is required for Google. Make sure to provide them in the options." ); throw new index.BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED"); } if (!codeVerifier) { throw new index.BetterAuthError("codeVerifier is required for Google"); } const _scopes = options.disableDefaultScope ? [] : ["email", "profile", "openid"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); const url = await refreshAccessToken.createAuthorizationURL({ id: "google", options, authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth", scopes: _scopes, state, codeVerifier, redirectURI, prompt: options.prompt, accessType: options.accessType, display: display || options.display, loginHint, hd: options.hd }); return url; }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint: "https://oauth2.googleapis.com/token" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token" }); }, async verifyIdToken(token, nonce) { if (options.disableIdTokenSignIn) { return false; } if (options.verifyIdToken) { return options.verifyIdToken(token, nonce); } const googlePublicKeyUrl = `https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${token}`; const { data: tokenInfo } = await fetch.betterFetch(googlePublicKeyUrl); if (!tokenInfo) { return false; } const isValid = tokenInfo.aud === options.clientId && (tokenInfo.iss === "https://accounts.google.com" || tokenInfo.iss === "accounts.google.com"); return isValid; }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (!token.idToken) { return null; } const user = jose.decodeJwt(token.idToken); const userMap = await options.mapProfileToUser?.(user); return { user: { id: user.sub, name: user.name, email: user.email, image: user.picture, emailVerified: user.email_verified, ...userMap }, data: user }; }, options }; }; const microsoft = (options) => { const tenant = options.tenantId || "common"; const authorizationEndpoint = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`; const tokenEndpoint = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`; return { id: "microsoft", name: "Microsoft EntraID", createAuthorizationURL(data) { const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"]; options.scope && scopes.push(...options.scope); data.scopes && scopes.push(...scopes); return refreshAccessToken.createAuthorizationURL({ id: "microsoft", options, authorizationEndpoint, state: data.state, codeVerifier: data.codeVerifier, scopes, redirectURI: data.redirectURI, prompt: options.prompt }); }, validateAuthorizationCode({ code, codeVerifier, redirectURI }) { return refreshAccessToken.validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } if (!token.idToken) { return null; } const user = jose.decodeJwt(token.idToken); const profilePhotoSize = options.profilePhotoSize || 48; await fetch.betterFetch( `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`, { headers: { Authorization: `Bearer ${token.accessToken}` }, async onResponse(context) { if (options.disableProfilePhoto || !context.response.ok) { return; } try { const response = context.response.clone(); const pictureBuffer = await response.arrayBuffer(); const pictureBase64 = base64.base64.encode(pictureBuffer); user.picture = `data:image/jpeg;base64, ${pictureBase64}`; } catch (e) { logger.logger.error( e && typeof e === "object" && "name" in e ? e.name : "", e ); } } } ); const userMap = await options.mapProfileToUser?.(user); return { user: { id: user.sub, name: user.name, email: user.email, image: user.picture, emailVerified: true, ...userMap }, data: user }; }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { const scopes = options.disableDefaultScope ? [] : ["openid", "profile", "email", "User.Read", "offline_access"]; options.scope && scopes.push(...options.scope); return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientSecret: options.clientSecret }, extraParams: { scope: scopes.join(" ") // Include the scopes in request to microsoft }, tokenEndpoint }); }, options }; }; const spotify = (options) => { return { id: "spotify", name: "Spotify", createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["user-read-email"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return refreshAccessToken.createAuthorizationURL({ id: "spotify", options, authorizationEndpoint: "https://accounts.spotify.com/authorize", scopes: _scopes, state, codeVerifier, redirectURI }); }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint: "https://accounts.spotify.com/api/token" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://accounts.spotify.com/api/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://api.spotify.com/v1/me", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.display_name, email: profile.email, image: profile.images[0]?.url, emailVerified: false, ...userMap }, data: profile }; }, options }; }; const twitch = (options) => { return { id: "twitch", name: "Twitch", createAuthorizationURL({ state, scopes, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["user:read:email", "openid"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return refreshAccessToken.createAuthorizationURL({ id: "twitch", redirectURI, options, authorizationEndpoint: "https://id.twitch.tv/oauth2/authorize", scopes: _scopes, state, claims: options.claims || [ "email", "email_verified", "preferred_username", "picture" ] }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint: "https://id.twitch.tv/oauth2/token" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://id.twitch.tv/oauth2/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const idToken = token.idToken; if (!idToken) { logger.logger.error("No idToken found in token"); return null; } const profile = jose.decodeJwt(idToken); const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.sub, name: profile.preferred_username, email: profile.email, image: profile.picture, emailVerified: false, ...userMap }, data: profile }; }, options }; }; const twitter = (options) => { return { id: "twitter", name: "Twitter", createAuthorizationURL(data) { const _scopes = options.disableDefaultScope ? [] : ["users.read", "tweet.read", "offline.access", "users.email"]; options.scope && _scopes.push(...options.scope); data.scopes && _scopes.push(...data.scopes); return refreshAccessToken.createAuthorizationURL({ id: "twitter", options, authorizationEndpoint: "https://x.com/i/oauth2/authorize", scopes: _scopes, state: data.state, codeVerifier: data.codeVerifier, redirectURI: data.redirectURI }); }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, codeVerifier, authentication: "basic", redirectURI, options, tokenEndpoint: "https://api.x.com/2/oauth2/token" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://api.x.com/2/oauth2/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error: profileError } = await fetch.betterFetch( "https://api.x.com/2/users/me?user.fields=profile_image_url", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}` } } ); if (profileError) { return null; } const { data: emailData, error: emailError } = await fetch.betterFetch("https://api.x.com/2/users/me?user.fields=confirmed_email", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}` } }); if (!emailError && emailData?.data?.confirmed_email) { profile.data.email = emailData.data.confirmed_email; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.data.id, name: profile.data.name, email: profile.data.email || profile.data.username || null, image: profile.data.profile_image_url, emailVerified: profile.data.verified || false, ...userMap }, data: profile }; }, options }; }; const dropbox = (options) => { const tokenEndpoint = "https://api.dropboxapi.com/oauth2/token"; return { id: "dropbox", name: "Dropbox", createAuthorizationURL: async ({ state, scopes, codeVerifier, redirectURI }) => { const _scopes = options.disableDefaultScope ? [] : ["account_info.read"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return await refreshAccessToken.createAuthorizationURL({ id: "dropbox", options, authorizationEndpoint: "https://www.dropbox.com/oauth2/authorize", scopes: _scopes, state, redirectURI, codeVerifier }); }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => { return await refreshAccessToken.validateAuthorizationCode({ code, codeVerifier, redirectURI, options, tokenEndpoint }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://api.dropbox.com/oauth2/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://api.dropboxapi.com/2/users/get_current_account", { method: "POST", headers: { Authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.account_id, name: profile.name?.display_name, email: profile.email, emailVerified: profile.email_verified || false, image: profile.profile_photo_url, ...userMap }, data: profile }; }, options }; }; const linkedin = (options) => { const authorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization"; const tokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken"; return { id: "linkedin", name: "Linkedin", createAuthorizationURL: async ({ state, scopes, redirectURI, loginHint }) => { const _scopes = options.disableDefaultScope ? [] : ["profile", "email", "openid"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return await refreshAccessToken.createAuthorizationURL({ id: "linkedin", options, authorizationEndpoint, scopes: _scopes, state, loginHint, redirectURI }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return await refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://api.linkedin.com/v2/userinfo", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.sub, name: profile.name, email: profile.email, emailVerified: profile.email_verified || false, image: profile.picture, ...userMap }, data: profile }; }, options }; }; const cleanDoubleSlashes = (input = "") => { return input.split("://").map((str) => str.replace(/\/{2,}/g, "/")).join("://"); }; const issuerToEndpoints = (issuer) => { let baseUrl = issuer || "https://gitlab.com"; return { authorizationEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/authorize`), tokenEndpoint: cleanDoubleSlashes(`${baseUrl}/oauth/token`), userinfoEndpoint: cleanDoubleSlashes(`${baseUrl}/api/v4/user`) }; }; const gitlab = (options) => { const { authorizationEndpoint, tokenEndpoint, userinfoEndpoint } = issuerToEndpoints(options.issuer); const issuerId = "gitlab"; const issuerName = "Gitlab"; return { id: issuerId, name: issuerName, createAuthorizationURL: async ({ state, scopes, codeVerifier, loginHint, redirectURI }) => { const _scopes = options.disableDefaultScope ? [] : ["read_user"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return await refreshAccessToken.createAuthorizationURL({ id: issuerId, options, authorizationEndpoint, scopes: _scopes, state, redirectURI, codeVerifier, loginHint }); }, validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, codeVerifier, tokenEndpoint }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://gitlab.com/oauth/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( userinfoEndpoint, { headers: { authorization: `Bearer ${token.accessToken}` } } ); if (error || profile.state !== "active" || profile.locked) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id.toString(), name: profile.name ?? profile.username, email: profile.email, image: profile.avatar_url, emailVerified: true, ...userMap }, data: profile }; }, options }; }; const tiktok = (options) => { return { id: "tiktok", name: "TikTok", createAuthorizationURL({ state, scopes, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["user.info.profile"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return new URL( `https://www.tiktok.com/v2/auth/authorize?scope=${_scopes.join( "," )}&response_type=code&client_key=${options.clientKey}&client_secret=${options.clientSecret}&redirect_uri=${encodeURIComponent( options.redirectURI || redirectURI )}&state=${state}` ); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI: options.redirectURI || redirectURI, options, tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://open.tiktokapis.com/v2/oauth/token/" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const fields = [ "open_id", "avatar_large_url", "display_name", "username" ]; const { data: profile, error } = await fetch.betterFetch( `https://open.tiktokapis.com/v2/user/info/?fields=${fields.join(",")}`, { headers: { authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } return { user: { email: profile.data.user.email || profile.data.user.username, id: profile.data.user.open_id, name: profile.data.user.display_name || profile.data.user.username, image: profile.data.user.avatar_large_url, /** @note Tiktok does not provide emailVerified or even email*/ emailVerified: profile.data.user.email ? true : false }, data: profile }; }, options }; }; const reddit = (options) => { return { id: "reddit", name: "Reddit", createAuthorizationURL({ state, scopes, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["identity"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return refreshAccessToken.createAuthorizationURL({ id: "reddit", options, authorizationEndpoint: "https://www.reddit.com/api/v1/authorize", scopes: _scopes, state, redirectURI, duration: options.duration }); }, validateAuthorizationCode: async ({ code, redirectURI }) => { const body = new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: options.redirectURI || redirectURI }); const headers = { "content-type": "application/x-www-form-urlencoded", accept: "text/plain", "user-agent": "better-auth", Authorization: `Basic ${base64.base64.encode( `${options.clientId}:${options.clientSecret}` )}` }; const { data, error } = await fetch.betterFetch( "https://www.reddit.com/api/v1/access_token", { method: "POST", headers, body: body.toString() } ); if (error) { throw error; } return refreshAccessToken.getOAuth2Tokens(data); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://www.reddit.com/api/v1/access_token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://oauth.reddit.com/api/v1/me", { headers: { Authorization: `Bearer ${token.accessToken}`, "User-Agent": "better-auth" } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.name, email: profile.oauth_client_id, emailVerified: profile.has_verified_email, image: profile.icon_img?.split("?")[0], ...userMap }, data: profile }; }, options }; }; const roblox = (options) => { return { id: "roblox", name: "Roblox", createAuthorizationURL({ state, scopes, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["openid", "profile"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return new URL( `https://apis.roblox.com/oauth/v1/authorize?scope=${_scopes.join( "+" )}&response_type=code&client_id=${options.clientId}&redirect_uri=${encodeURIComponent( options.redirectURI || redirectURI )}&state=${state}&prompt=${options.prompt || "select_account+consent"}` ); }, validateAuthorizationCode: async ({ code, redirectURI }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI: options.redirectURI || redirectURI, options, tokenEndpoint: "https://apis.roblox.com/oauth/v1/token", authentication: "post" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://apis.roblox.com/oauth/v1/token" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://apis.roblox.com/oauth/v1/userinfo", { headers: { authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.sub, name: profile.nickname || profile.preferred_username || "", image: profile.picture, email: profile.preferred_username || null, // Roblox does not provide email emailVerified: true, ...userMap }, data: { ...profile } }; }, options }; }; var LANG = /* @__PURE__ */ ((LANG2) => { LANG2[LANG2["RUS"] = 0] = "RUS"; LANG2[LANG2["UKR"] = 1] = "UKR"; LANG2[LANG2["ENG"] = 3] = "ENG"; LANG2[LANG2["SPA"] = 4] = "SPA"; LANG2[LANG2["GERMAN"] = 6] = "GERMAN"; LANG2[LANG2["POL"] = 15] = "POL"; LANG2[LANG2["FRA"] = 16] = "FRA"; LANG2[LANG2["TURKEY"] = 82] = "TURKEY"; return LANG2; })(LANG || {}); const vk = (options) => { return { id: "vk", name: "VK", async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) { const _scopes = options.disableDefaultScope ? [] : ["email", "phone"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); const authorizationEndpoint = "https://id.vk.com/authorize"; return refreshAccessToken.createAuthorizationURL({ id: "vk", options, authorizationEndpoint, scopes: _scopes, state, redirectURI, codeVerifier }); }, validateAuthorizationCode: async ({ code, codeVerifier, redirectURI, deviceId }) => { return refreshAccessToken.validateAuthorizationCode({ code, codeVerifier, redirectURI: options.redirectURI || redirectURI, options, deviceId, tokenEndpoint: "https://id.vk.com/oauth2/auth" }); }, refreshAccessToken: options.refreshAccessToken ? options.refreshAccessToken : async (refreshToken) => { return refreshAccessToken.refreshAccessToken({ refreshToken, options: { clientId: options.clientId, clientKey: options.clientKey, clientSecret: options.clientSecret }, tokenEndpoint: "https://id.vk.com/oauth2/auth" }); }, async getUserInfo(data) { if (options.getUserInfo) { return options.getUserInfo(data); } if (!data.accessToken) { return null; } const formBody = new URLSearchParams({ access_token: data.accessToken, client_id: options.clientId }).toString(); const { data: profile, error } = await fetch.betterFetch( "https://id.vk.com/oauth2/user_info", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formBody } ); if (error) { return null; } if (!profile.user.email) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.user.user_id, first_name: profile.user.first_name, last_name: profile.user.last_name, email: profile.user.email, image: profile.user.avatar, /** @note VK does not provide emailVerified*/ emailVerified: !!profile.user.email, birthday: profile.user.birthday, sex: profile.user.sex, ...userMap }, data: profile }; }, options }; }; const kick = (options) => { return { id: "kick", name: "Kick", createAuthorizationURL({ state, scopes, redirectURI, codeVerifier }) { const _scopes = options.disableDefaultScope ? [] : ["user:read"]; options.scope && _scopes.push(...options.scope); scopes && _scopes.push(...scopes); return refreshAccessToken.createAuthorizationURL({ id: "kick", redirectURI, options, authorizationEndpoint: "https://id.kick.com/oauth/authorize", scopes: _scopes, codeVerifier, state }); }, async validateAuthorizationCode({ code, redirectURI, codeVerifier }) { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI, options, tokenEndpoint: "https://id.kick.com/oauth/token", codeVerifier }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data, error } = await fetch.betterFetch("https://api.kick.com/public/v1/users", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}` } }); if (error) { return null; } const profile = data.data[0]; const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.user_id, name: profile.name, email: profile.email, image: profile.profile_picture, emailVerified: true, ...userMap }, data: profile }; }, options }; }; const zoom = (userOptions) => { const options = { pkce: true, ...userOptions }; return { id: "zoom", name: "Zoom", createAuthorizationURL: async ({ state, redirectURI, codeVerifier }) => { const params = new URLSearchParams({ response_type: "code", redirect_uri: options.redirectURI ? options.redirectURI : redirectURI, client_id: options.clientId, state }); if (options.pkce) { const codeChallenge = await refreshAccessToken.generateCodeChallenge(codeVerifier); params.set("code_challenge_method", "S256"); params.set("code_challenge", codeChallenge); } const url = new URL("https://zoom.us/oauth/authorize"); url.search = params.toString(); return url; }, validateAuthorizationCode: async ({ code, redirectURI, codeVerifier }) => { return refreshAccessToken.validateAuthorizationCode({ code, redirectURI: options.redirectURI || redirectURI, codeVerifier, options, tokenEndpoint: "https://zoom.us/oauth/token", authentication: "post" }); }, async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error } = await fetch.betterFetch( "https://api.zoom.us/v2/users/me", { headers: { authorization: `Bearer ${token.accessToken}` } } ); if (error) { return null; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.id, name: profile.display_name, image: profile.pic_url, email: profile.email, emailVerified: Boolean(profile.verified), ...userMap }, data: { ...profile } }; } }; }; const socialProviders = { apple, discord, facebook, github, microsoft, google, spotify, twitch, twitter, dropbox, kick, linkedin, gitlab, tiktok, reddit, roblox, vk, zoom }; const socialProviderList = Object.keys(socialProviders); const SocialProviderListEnum = zod.z.enum(socialProviderList).or(zod.z.string()); exports.LANG = LANG; exports.SocialProviderListEnum = SocialProviderListEnum; exports.apple = apple; exports.discord = discord; exports.dropbox = dropbox; exports.facebook = facebook; exports.getApplePublicKey = getApplePublicKey; exports.github = github; exports.gitlab = gitlab; exports.google = google; exports.kick = kick; exports.linkedin = linkedin; exports.microsoft = microsoft; exports.reddit = reddit; exports.roblox = roblox; exports.socialProviderList = socialProviderList; exports.socialProviders = socialProviders; exports.spotify = spotify; exports.tiktok = tiktok; exports.twitch = twitch; exports.twitter = twitter; exports.vk = vk; exports.zoom = zoom;