UNPKG

payload-auth-plugin

Version:
1,709 lines (1,681 loc) 58 kB
// src/core/errors/consoleErrors.ts var PluginError = class extends Error { constructor(message, cause) { super(message); this.name = "PAYLOAD_AUTH_PLUGIN_ERROR"; this.message = message; this.cause = cause; this.stack = ""; } }; var InvalidServerURL = class extends PluginError { constructor() { super( "Missing or invalid server URL. Please set serverURL in your Payload config" ); } }; var InvalidProvider = class extends PluginError { constructor() { super("Invalid Provider"); } }; var ProviderAlreadyExists = class extends PluginError { constructor() { super("Duplicate provider found"); } }; var InvalidOAuthAlgorithm = class extends PluginError { constructor() { super( "Invalid OAuth Algorithm. Plugin only support OIDC and OAuth2 algorithms" ); } }; var InvalidOAuthResource = class extends PluginError { constructor() { super("Invalid resource request. Check docs before initiating requests"); } }; var MissingOrInvalidSession = class extends PluginError { constructor() { super("Missing or invalid session."); } }; var InvalidCollectionSlug = class extends PluginError { constructor() { super("Missing or invalid collection slug"); } }; var MissingCollections = class extends PluginError { constructor() { super("Missing collections"); } }; var MissingEmailAdapter = class extends PluginError { constructor() { super( "Email adapter is required. Check the docs for the setup: https://payloadcms.com/docs/email/overview" ); } }; // src/providers/utils.ts function getOAuthProviders(providers) { const records = {}; providers.map((provider) => { if (records[provider.id]) { throw new ProviderAlreadyExists(); } if (provider.kind === "oauth") { records[provider.id] = provider; } }); return records; } function getPasskeyProvider(providers) { const passkeyProvider = providers.find( (provider) => provider.kind === "passkey" ); if (passkeyProvider) { return passkeyProvider; } return null; } function getPasswordProvider(providers) { const provider = providers.find((provider2) => provider2.kind === "password"); if (provider) { return provider; } return null; } function generateProviderCustomEmail(prefix, domain) { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).slice(2, 8); return `${prefix}_${timestamp}${random}@${domain}`; } // src/core/routeHandlers/oauth.ts import { parseCookies as parseCookies3 } from "payload"; // src/core/protocols/oauth/oauth2_authorization.ts import * as oauth from "oauth4webapi"; // src/core/utils/cb.ts function getCallbackURL(baseURL, pluginType, provider) { const callback_url = new URL(baseURL); callback_url.pathname = `/api/${pluginType}/oauth/callback/${provider}`; callback_url.search = ""; return callback_url; } // src/core/protocols/oauth/oauth2_authorization.ts async function OAuth2Authorization(pluginType, request, providerConfig, clientOrigin, additionalScope) { const callback_url = getCallbackURL( request.payload.config.serverURL, pluginType, providerConfig.id ); const code_verifier = oauth.generateRandomCodeVerifier(); const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier); const code_challenge_method = "S256"; const { authorization_server, client_id, scope, params } = providerConfig; const client = { client_id }; const as = authorization_server; const cookies = []; const cookieMaxage = new Date(Date.now() + 300 * 1e3); const authorizationURL = new URL(as.authorization_endpoint); authorizationURL.searchParams.set("client_id", client.client_id); authorizationURL.searchParams.set("redirect_uri", callback_url.toString()); authorizationURL.searchParams.set("response_type", "code"); if (additionalScope) { const totalScope = `${scope} ${additionalScope}`; authorizationURL.searchParams.set("scope", totalScope); } else { authorizationURL.searchParams.set("scope", scope); } authorizationURL.searchParams.set("code_challenge", code_challenge); authorizationURL.searchParams.set( "code_challenge_method", code_challenge_method ); if (params) { Object.entries(params).map(([key, value]) => { authorizationURL.searchParams.set(key, value); }); } if (as.code_challenge_methods_supported?.includes("S256") !== true) { const state = oauth.generateRandomState(); authorizationURL.searchParams.set("state", state); cookies.push( `__session-oauth-state=${state};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); } cookies.push( `__session-code-verifier=${code_verifier};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); if (clientOrigin && clientOrigin !== void 0) { cookies.push( `__session-client-origin=${clientOrigin};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); } const res = new Response(null, { status: 302, headers: { Location: authorizationURL.href } }); for (const c of cookies) { res.headers.append("Set-Cookie", c); } return res; } // src/core/protocols/oauth/oauth2_callback.ts import * as oauth2 from "oauth4webapi"; import { parseCookies } from "payload"; // src/core/protocols/oauth/oauth_authentication.ts import * as jose from "jose"; import { generatePayloadCookie, getFieldsToSign, jwtSign } from "payload"; // src/core/errors/apiErrors.ts var statusByKind = { ["NotFound" /* NotFound */]: 404, ["BadRequest" /* BadRequest */]: 400, ["InternalServer" /* InternalServer */]: 500, ["NotAuthenticated" /* NotAuthenticated */]: 401, ["NotAuthorized" /* NotAuthorized */]: 403, ["Conflict" /* Conflict */]: 409 }; var AuthAPIError = class extends Response { constructor(message, kind) { super( JSON.stringify({ message, kind, data: null, isSuccess: false, isError: true }), { status: statusByKind[kind] } ); } }; var MissingEmailAPIError = class extends AuthAPIError { constructor() { super("Missing email. Email is required", "BadRequest" /* BadRequest */); } }; var UnVerifiedAccountAPIError = class extends AuthAPIError { constructor() { super("Account is not verified", "BadRequest" /* BadRequest */); } }; var UserNotFoundAPIError = class extends AuthAPIError { constructor() { super("User not found", "NotFound" /* NotFound */); } }; var PasskeyVerificationAPIError = class extends AuthAPIError { constructor() { super("Passkey verification failed", "BadRequest" /* BadRequest */); } }; var InvalidAPIRequest = class extends AuthAPIError { constructor() { super("Invalid API request", "BadRequest" /* BadRequest */); } }; var UnauthorizedAPIRequest = class extends AuthAPIError { constructor() { super("Unauthorized access", "NotAuthorized" /* NotAuthorized */); } }; var InvalidCredentials = class extends AuthAPIError { constructor() { super("Invalid Credentials", "BadRequest" /* BadRequest */); } }; var InvalidRequestBodyError = class extends AuthAPIError { constructor() { super("Wrong request body. Missing parameters", "BadRequest" /* BadRequest */); } }; var EmailAlreadyExistError = class extends AuthAPIError { constructor() { super("Email is already taken", "Conflict" /* Conflict */); } }; var InternalServerError = class extends AuthAPIError { constructor() { super("Something went wrong. Server failure", "BadRequest" /* BadRequest */); } }; var MissingOrInvalidVerification = class extends AuthAPIError { constructor() { super( "Verification failed. Missing or invalid verification code.", "BadRequest" /* BadRequest */ ); } }; var MissingCollection = class extends AuthAPIError { constructor() { super("Missing collection", "NotFound" /* NotFound */); } }; // src/core/protocols/oauth/oauth_authentication.ts import { v4 as uuid } from "uuid"; // src/core/utils/session.ts var removeExpiredSessions = (sessions) => { const now = /* @__PURE__ */ new Date(); return sessions.filter(({ expiresAt }) => { const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt); return expiry > now; }); }; // src/core/protocols/oauth/oauth_authentication.ts async function _createUser({ email, name, collections, request, allowOAuthAutoSignUp }) { const { payload } = request; let userRecord = await payload.db.findOne({ collection: collections.usersCollection, where: { email: { equals: email } }, req: request }); if (!userRecord && allowOAuthAutoSignUp) { const data = { email, name }; const hasAuthEnabled = Boolean( payload.collections[collections.usersCollection].config.auth ); if (hasAuthEnabled) { data.password = jose.base64url.encode( crypto.getRandomValues(new Uint8Array(16)) ); } userRecord = await payload.db.create({ collection: collections.usersCollection, data, returning: true }); } else { return null; } return userRecord; } async function OAuthAuthentication(pluginType, collections, allowOAuthAutoSignUp, useAdmin, secret, request, successRedirectPath, errorRedirectPath, account) { const { email: _email, sub, name, scope, issuer, picture, access_token, refresh_token, expires_in, claims } = account; const { payload } = request; const trxID = await payload.db.beginTransaction(); let userRecord = null; const accountRecords = await payload.db.find({ collection: collections.accountsCollection, where: { sub: { equals: sub } }, req: request }); if (accountRecords.docs && accountRecords.docs.length === 1) { if (accountRecords.docs[0].user) { userRecord = await payload.db.findOne({ collection: collections.usersCollection, where: { id: { equals: accountRecords.docs[0].user } }, req: request }); } await payload.db.updateOne({ collection: collections.accountsCollection, id: accountRecords.docs[0].id, data: { scope, name, picture, issuerName: issuer, access_token, refresh_token, expires_in }, req: request }); } else { userRecord = await _createUser({ email: _email.toLowerCase(), name, request, collections, allowOAuthAutoSignUp }); if (userRecord) { await payload.db.create({ collection: collections.accountsCollection, data: { scope, name, picture, issuerName: issuer, access_token, refresh_token, expires_in, sub, user: userRecord.id }, req: request }); } } if (!userRecord) { if (trxID) { await payload.db.rollbackTransaction(trxID); } return new UserNotFoundAPIError(); } const collectionConfig = payload.config.collections.find( (collection) => collection.slug === collections.usersCollection ); if (!collectionConfig) { if (trxID) { await payload.db.rollbackTransaction(trxID); } return new MissingCollection(); } const sessionID = collectionConfig?.auth.useSessions ? uuid() : null; if (collectionConfig?.auth.useSessions) { const now = /* @__PURE__ */ new Date(); const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1e3; const expiresAt = new Date(now.getTime() + tokenExpInMs); const session = { id: sessionID, createdAt: now, expiresAt }; if (!userRecord?.sessions?.length) { userRecord.sessions = [session]; } else { userRecord.sessions = removeExpiredSessions(userRecord.sessions); userRecord.sessions.push(session); } userRecord.updatedAt = null; const r = await payload.db.updateOne({ id: userRecord.id, collection: collectionConfig.slug, data: userRecord, req: request, returning: true }); userRecord.collection = collectionConfig.slug; userRecord._strategy = "local-jwt"; } const claimUser = await payload.db.findOne({ collection: collections.usersCollection, where: { id: { equals: userRecord.id } }, req: request }); const fieldsToSign = getFieldsToSign({ user: claimUser, email: _email.toLowerCase(), sid: sessionID ?? void 0, collectionConfig }); if (collectionConfig.hooks?.beforeLogin?.length) { for (const hook of collectionConfig.hooks.beforeLogin) { userRecord = await hook({ collection: collectionConfig, context: request.context, req: request, user: claimUser }) || userRecord; } } const { exp, token } = await jwtSign({ fieldsToSign, secret, tokenExpiration: collectionConfig.auth.tokenExpiration }); if (collectionConfig.hooks?.afterLogin?.length) { for (const hook of collectionConfig.hooks.afterLogin) { userRecord = await hook({ collection: collectionConfig, context: request.context, req: request, token, user: userRecord }) || userRecord; } } if (collectionConfig.hooks?.afterRead?.length) { for (const hook of collectionConfig.hooks.afterRead) { userRecord = await hook({ collection: collectionConfig, context: request.context, doc: userRecord, req: request }) || userRecord; } } const successRedirectionURL = new URL( `${payload.config.serverURL}${successRedirectPath}` ); let result = { exp, token, user: userRecord }; const cookie = generatePayloadCookie({ collectionAuthConfig: collectionConfig.auth, cookiePrefix: useAdmin ? `${payload.config.cookiePrefix}` : `__${pluginType}`, token: result.token }); if (trxID) { await payload.db.commitTransaction(trxID); } const res = new Response(null, { status: 302, headers: { Location: successRedirectionURL.href } }); res.headers.append("Set-Cookie", cookie); return res; } // src/core/protocols/oauth/oauth2_callback.ts async function OAuth2Callback(pluginType, request, providerConfig, collections, allowOAuthAutoSignUp, useAdmin, secret, successRedirectPath, errorRedirectPath, additionalScope) { const parsedCookies = parseCookies(request.headers); const code_verifier = parsedCookies.get("__session-code-verifier"); const state = parsedCookies.get("__session-oauth-state"); if (!code_verifier) { throw new MissingOrInvalidSession(); } const { client_id, client_secret, authorization_server, client_auth_type } = providerConfig; const client = { client_id }; const clientAuth = client_auth_type === "client_secret_basic" ? oauth2.ClientSecretBasic(client_secret ?? "") : oauth2.ClientSecretPost(client_secret ?? ""); const current_url = new URL(request.url); const callback_url = getCallbackURL( request.payload.config.serverURL, pluginType, providerConfig.id ); const as = authorization_server; const params = oauth2.validateAuthResponse(as, client, current_url, state); const grantResponse = await oauth2.authorizationCodeGrantRequest( as, client, clientAuth, params, callback_url.toString(), code_verifier ); const body = await grantResponse.json(); let response = new Response(JSON.stringify(body), grantResponse); if (Array.isArray(body.scope)) { body.scope = body.scope.join(" "); response = new Response(JSON.stringify(body), grantResponse); } const token_result = await oauth2.processAuthorizationCodeResponse( as, client, response ); const userInfoResponse = await oauth2.userInfoRequest( as, client, token_result.access_token ); const userInfo = await userInfoResponse.json(); const email = providerConfig.emailDomain ? generateProviderCustomEmail(providerConfig.name.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-"), providerConfig.emailDomain) : userInfo.email; const userData = { email, name: userInfo.name ?? "", sub: userInfo.sub, scope: providerConfig.scope + (additionalScope ? ` ${additionalScope}` : ""), issuer: providerConfig.authorization_server.issuer, picture: userInfo.picture ?? "", access_token: token_result.access_token, refresh_token: token_result.refresh_token ?? "", expires_in: typeof token_result.expires_in === "number" ? token_result.expires_in : void 0, claims: {} // TODO: Take a look how claims work with OAuth2 }; return await OAuthAuthentication( pluginType, collections, allowOAuthAutoSignUp, useAdmin, secret, request, successRedirectPath, errorRedirectPath, userData ); } // src/core/protocols/oauth/oidc_authorization.ts import * as oauth3 from "oauth4webapi"; async function OIDCAuthorization(pluginType, request, providerConfig, additionalScope) { const callback_url = getCallbackURL( request.payload.config.serverURL, pluginType, providerConfig.id ); const code_verifier = oauth3.generateRandomCodeVerifier(); const code_challenge = await oauth3.calculatePKCECodeChallenge(code_verifier); const code_challenge_method = "S256"; const { client_id, issuer, algorithm, scope, params } = providerConfig; const client = { client_id }; const issuer_url = new URL(issuer); const as = await oauth3.discoveryRequest(issuer_url, { algorithm }).then((response) => oauth3.processDiscoveryResponse(issuer_url, response)); const cookies = []; const cookieMaxage = new Date(Date.now() + 300 * 1e3); const authorizationURL = new URL(as.authorization_endpoint); authorizationURL.searchParams.set("client_id", client.client_id); authorizationURL.searchParams.set("redirect_uri", callback_url.toString()); authorizationURL.searchParams.set("response_type", "code"); if (additionalScope) { const totalScope = `${scope} ${additionalScope}`; authorizationURL.searchParams.set("scope", totalScope); } else { authorizationURL.searchParams.set("scope", scope); } authorizationURL.searchParams.set("code_challenge", code_challenge); authorizationURL.searchParams.set( "code_challenge_method", code_challenge_method ); if (params) { Object.entries(params).map(([key, value]) => { authorizationURL.searchParams.set(key, value); }); } if (as.code_challenge_methods_supported?.includes("S256") !== true) { const nonce = oauth3.generateRandomNonce(); authorizationURL.searchParams.set("nonce", nonce); cookies.push( `__session-oauth-nonce=${nonce};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); } cookies.push( `__session-code-verifier=${code_verifier};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); const res = new Response(null, { status: 302, headers: { Location: authorizationURL.href } }); for (const c of cookies) { res.headers.append("Set-Cookie", c); } return res; } // src/core/protocols/oauth/oidc_callback.ts import * as oauth4 from "oauth4webapi"; import { parseCookies as parseCookies2 } from "payload"; async function OIDCCallback(pluginType, request, providerConfig, collections, allowOAuthAutoSignUp, useAdmin, secret, successRedirectPath, errorRedirectPath, additionalScope) { const parsedCookies = parseCookies2(request.headers); const code_verifier = parsedCookies.get("__session-code-verifier"); const nonce = parsedCookies.get("__session-oauth-nonce"); if (!code_verifier) { throw new MissingOrInvalidSession(); } const { client_id, client_secret, issuer, algorithm, profile } = providerConfig; const client = { client_id }; const clientAuth = oauth4.ClientSecretPost(client_secret ?? ""); const current_url = new URL(request.url); const callback_url = getCallbackURL( request.payload.config.serverURL, pluginType, providerConfig.id ); const issuer_url = new URL(issuer); const as = await oauth4.discoveryRequest(issuer_url, { algorithm }).then((response2) => oauth4.processDiscoveryResponse(issuer_url, response2)); const params = oauth4.validateAuthResponse( as, client, current_url, providerConfig?.params?.state || void 0 ); const grantResponse = await oauth4.authorizationCodeGrantRequest( as, client, clientAuth, params, callback_url.toString(), code_verifier ); const body = await grantResponse.json(); let response = new Response(JSON.stringify(body), grantResponse); if (Array.isArray(body.scope)) { body.scope = body.scope.join(" "); response = new Response(JSON.stringify(body), grantResponse); } const token_result = await oauth4.processAuthorizationCodeResponse( as, client, response, { expectedNonce: nonce, requireIdToken: true } ); const claims = oauth4.getValidatedIdTokenClaims(token_result); if (!claims?.sub) { return new InternalServerError(); } const userInfoResponse = await oauth4.userInfoRequest( as, client, token_result.access_token ); const result = await oauth4.processUserInfoResponse( as, client, claims?.sub, userInfoResponse ); const email = providerConfig.emailDomain && !result.email ? generateProviderCustomEmail(providerConfig.name.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-"), providerConfig.emailDomain) : result.email; if (!email) { return new MissingEmailAPIError(); } if (!providerConfig.skip_email_verification && !result.email_verified) { return new UnVerifiedAccountAPIError(); } const userData = { email, name: result.name ?? "", sub: result.sub, scope: providerConfig.scope + (additionalScope ? ` ${additionalScope}` : ""), issuer: providerConfig.issuer, picture: result.picture ?? "", access_token: token_result.access_token, refresh_token: token_result.refresh_token ?? "", expires_in: typeof token_result.expires_in === "number" ? token_result.expires_in : void 0, claims }; return await OAuthAuthentication( pluginType, collections, allowOAuthAutoSignUp, useAdmin, secret, request, successRedirectPath, errorRedirectPath, userData ); } // src/core/routeHandlers/oauth.ts function OAuthHandlers(pluginType, collections, allowOAuthAutoSignUp, secret, useAdmin, request, provider, successRedirectPath, errorRedirectPath) { if (!provider) { throw new InvalidProvider(); } const resource = request.routeParams?.resource; const headers = request.headers; const cookies = parseCookies3(headers); const additionalScope = cookies.get("oauth_scope"); switch (resource) { case "authorization": switch (provider.algorithm) { case "oidc": return OIDCAuthorization( pluginType, request, provider, additionalScope ); case "oauth2": return OAuth2Authorization( pluginType, request, provider, additionalScope ); default: throw new InvalidOAuthAlgorithm(); } case "callback": switch (provider.algorithm) { case "oidc": { return OIDCCallback( pluginType, request, provider, collections, allowOAuthAutoSignUp, useAdmin, secret, successRedirectPath, errorRedirectPath, additionalScope ); } case "oauth2": { return OAuth2Callback( pluginType, request, provider, collections, allowOAuthAutoSignUp, useAdmin, secret, successRedirectPath, errorRedirectPath, additionalScope ); } default: throw new InvalidOAuthAlgorithm(); } default: throw new InvalidOAuthResource(); } } // src/core/utils/hash.ts import * as jose2 from "jose"; function hashCode(s) { let h = 0; const l = s.length; let i = 0; if (l > 0) while (i < l) h = (h << 5) + h + s.charCodeAt(i++) | 0; return h; } var ephemeralCode = async (length, secret) => { const code = []; while (code.length < length) { const buffer = crypto.getRandomValues(new Uint8Array(length * 2)); for (const byte of buffer) { if (byte < 250 && code.length < length) { code.push(byte % 10); } } } const codeStr = code.join(""); const iterations = 6e5; const encoder = new TextEncoder(); const bytes = encoder.encode(codeStr); const salt = encoder.encode(secret); const keyMaterial = await crypto.subtle.importKey( "raw", bytes, "PBKDF2", false, ["deriveBits"] ); const hash = await crypto.subtle.deriveBits( { name: "PBKDF2", hash: "SHA-256", salt, iterations }, keyMaterial, 256 ); const hashB64 = jose2.base64url.encode(new Uint8Array(hash)); return { hash: hashB64, code: codeStr }; }; var verifyEphemeralCode = async (code, hashB64, secret) => { const encoder = new TextEncoder(); const codeBytes = encoder.encode(code); const salt = encoder.encode(secret); const params = { name: "PBKDF2", hash: "SHA-256", salt, iterations: 6e5 }; const keyMaterial = await crypto.subtle.importKey( "raw", codeBytes, "PBKDF2", false, ["deriveBits"] ); const hash = await crypto.subtle.deriveBits(params, keyMaterial, 256); const hashBase64 = jose2.base64url.encode(new Uint8Array(hash)); return hashBase64 === hashB64; }; // src/core/protocols/passkey/index.ts async function InitPasskey(request) { const { data } = await request.json(); if (!data.email) { throw new MissingEmailAPIError(); } const existingRecord = await request.payload.find({ collection: "accounts", where: { sub: { equals: hashCode(data.email + request.payload.secret).toString() } } }); if (existingRecord.totalDocs !== 1) { return new Response(JSON.stringify({ data: {} }), { status: 200 }); } return new Response(JSON.stringify({ data: existingRecord.docs[0] }), { status: 200 }); } // src/core/protocols/passkey/registration.ts import { parseCookies as parseCookies4 } from "payload"; import { generateRegistrationOptions, verifyRegistrationResponse } from "@simplewebauthn/server"; async function GeneratePasskeyRegistration(request, rpID) { const { data } = await request.json(); const registrationOptions = { rpName: "Payload Passkey Webauth", rpID, userName: data.email, timeout: 6e4, attestationType: "none", authenticatorSelection: { residentKey: "required", userVerification: "required" }, supportedAlgorithmIDs: [-7, -257] }; const options = await generateRegistrationOptions(registrationOptions); const cookieMaxage = new Date(Date.now() + 300 * 1e3); const cookies = []; cookies.push( `__session-webpk-challenge=${options.challenge};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); const res = new Response(JSON.stringify({ options }), { status: 201 }); cookies.forEach((cookie) => { res.headers.append("Set-Cookie", cookie); }); return res; } async function VerifyPasskeyRegistration(request, rpID, session_callback) { try { const parsedCookies = parseCookies4(request.headers); const challenge = parsedCookies.get("__session-webpk-challenge"); if (!challenge) { throw new MissingOrInvalidSession(); } const body = await request.json(); const verification = await verifyRegistrationResponse({ response: body.data.registration, expectedChallenge: challenge, expectedOrigin: request.payload.config.serverURL, expectedRPID: rpID }); if (!verification.verified) { throw new PasskeyVerificationAPIError(); } const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo; return await session_callback({ sub: hashCode(body.data.email + request.payload.secret).toString(), name: "", picture: "", email: body.data.email, passKey: { credentialId: credential.id, publicKey: credential.publicKey, counter: credential.counter, transports: credential.transports, deviceType: credentialDeviceType, backedUp: credentialBackedUp } }); } catch (error) { console.error(error); return Response.json({}); } } // src/core/protocols/passkey/authentication.ts import { parseCookies as parseCookies5 } from "payload"; import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server"; async function GeneratePasskeyAuthentication(request, rpID) { const { data } = await request.json(); const registrationOptions = { rpID, timeout: 6e4, allowCredentials: [ { id: data.passkey.credentialId, transports: data.passkey.transports } ], userVerification: "required" }; const options = await generateAuthenticationOptions(registrationOptions); const cookieMaxage = new Date(Date.now() + 300 * 1e3); const cookies = []; cookies.push( `__session-webpk-challenge=${options.challenge};Path=/;HttpOnly;SameSite=lax;Expires=${cookieMaxage.toUTCString()}` ); const res = new Response(JSON.stringify({ options }), { status: 201 }); cookies.forEach((cookie) => { res.headers.append("Set-Cookie", cookie); }); return res; } async function VerifyPasskeyAuthentication(request, rpID, session_callback) { try { const parsedCookies = parseCookies5(request.headers); const challenge = parsedCookies.get("__session-webpk-challenge"); if (!challenge) { throw new MissingOrInvalidSession(); } const { data } = await request.json(); const verification = await verifyAuthenticationResponse({ response: data.authentication, expectedChallenge: challenge, expectedOrigin: request.payload.config.serverURL, expectedRPID: rpID, credential: { id: data.passkey.credentialId, publicKey: new Uint8Array(Object.values(data.passkey.publicKey)), counter: data.passkey.counter, transports: data.passkey.transports } }); if (!verification.verified) { throw new PasskeyVerificationAPIError(); } const { credentialID, credentialDeviceType, credentialBackedUp, newCounter } = verification.authenticationInfo; return await session_callback({ sub: hashCode(data.email + request.payload.secret).toString(), name: "", picture: "", email: data.email, passKey: { credentialId: credentialID, counter: newCounter, deviceType: credentialDeviceType, backedUp: credentialBackedUp } }); } catch (error) { console.error(error); return Response.json({}); } } // src/core/routeHandlers/passkey.ts function PasskeyHandlers(request, resource, rpID, sessionCallBack) { switch (resource) { case "init": return InitPasskey(request); case "generate-registration-options": return GeneratePasskeyRegistration(request, rpID); case "verify-registration": return VerifyPasskeyRegistration(request, rpID, sessionCallBack); case "generate-authentication-options": return GeneratePasskeyAuthentication(request, rpID); case "verify-authentication": return VerifyPasskeyAuthentication(request, rpID, sessionCallBack); default: throw new InvalidAPIRequest(); } } // src/core/protocols/password.ts import { parseCookies as parseCookies6 } from "payload"; import { v4 as uuid2 } from "uuid"; // src/constants.ts var APP_COOKIE_SUFFIX = "session-token"; // src/core/utils/cookies.ts import * as jwt from "jose"; import { getCookieExpiration, generateCookie } from "payload"; async function createSessionCookies(name, secret, fieldsToSign, expiration, collectionAuthConfig) { const tokenExpiration = expiration ?? getCookieExpiration({ seconds: 7200 }).getTime(); const secretKey = new TextEncoder().encode(secret); const issuedAt = Math.floor(Date.now() / 1e3); const exp = issuedAt + tokenExpiration; const token = await new jwt.SignJWT(fieldsToSign).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt(issuedAt).setExpirationTime(exp).sign(secretKey); const cookies = []; if (collectionAuthConfig) { const sameSite = typeof collectionAuthConfig.cookies.sameSite === "string" ? collectionAuthConfig.cookies.sameSite : collectionAuthConfig.cookies.sameSite ? "Strict" : void 0; const cookie = generateCookie({ name, domain: collectionAuthConfig.cookies.domain ?? void 0, expires: getCookieExpiration({ seconds: expiration }), httpOnly: true, path: "/", returnCookieAsObject: false, sameSite, secure: collectionAuthConfig.cookies.secure, value: token }); cookies.push(cookie); } else { cookies.push( `${name}=${token};Path=/;HttpOnly;SameSite=lax;Expires=${getCookieExpiration({ seconds: expiration }).toUTCString()}` ); } return cookies; } async function verifySessionCookie(token, secret) { const secretKey = new TextEncoder().encode(secret); return await jwt.jwtVerify(token, secretKey); } function invalidateOAuthCookies(cookies) { const expired = "Thu, 01 Jan 1970 00:00:00 GMT"; cookies.push( `__session-oauth-state=; Path=/; HttpOnly; SameSite=Lax; Expires=${expired}` ); cookies.push( `__session-oauth-nonce=; Path=/; HttpOnly; SameSite=Lax; Expires=${expired}` ); cookies.push( `__session-code-verifier=; Path=/; HttpOnly; SameSite=Lax; Expires=${expired}` ); cookies.push( `__session-webpk-challenge=; Path=/; HttpOnly; SameSite=Lax; Expires=${expired}` ); return cookies; } // src/core/utils/password.ts import * as jose3 from "jose"; var hashPassword = async (password) => { const iterations = 6e5; const encoder = new TextEncoder(); const bytes = encoder.encode(password); const salt = crypto.getRandomValues(new Uint8Array(16)); const keyMaterial = await crypto.subtle.importKey( "raw", bytes, "PBKDF2", false, ["deriveBits"] ); const hash = await crypto.subtle.deriveBits( { name: "PBKDF2", hash: "SHA-256", salt, iterations }, keyMaterial, 256 ); const hashB64 = jose3.base64url.encode(new Uint8Array(hash)); const saltB64 = jose3.base64url.encode(salt); return { hash: hashB64, salt: saltB64, iterations }; }; var verifyPassword = async (password, hashB64, saltB64, iterations) => { const encoder = new TextEncoder(); const passwordBytes = encoder.encode(password); const salt = jose3.base64url.decode(saltB64); const params = { name: "PBKDF2", hash: "SHA-256", salt, iterations }; const keyMaterial = await crypto.subtle.importKey( "raw", passwordBytes, "PBKDF2", false, ["deriveBits"] ); const hash = await crypto.subtle.deriveBits(params, keyMaterial, 256); const hashBase64 = jose3.base64url.encode(new Uint8Array(hash)); return hashBase64 === hashB64; }; // src/core/protocols/password.ts var redirectWithSession = async (cookieName, path, secret, fields, request, tokenExpiration) => { let cookies = []; cookies = [ ...await createSessionCookies( cookieName, secret, fields, tokenExpiration ) ]; cookies = invalidateOAuthCookies(cookies); const successRedirectionURL = new URL(`${request.origin}${path}`); const res = new Response(null, { status: 302, headers: { Location: successRedirectionURL.href } }); for (const c of cookies) { res.headers.append("Set-Cookie", c); } return res; }; var PasswordSignin = async (pluginType, request, internal, useAdmin, secret, successRedirectPath, errorRedirectPath) => { const body = request.json && await request.json(); if (!body?.email || !body.password) { return new InvalidRequestBodyError(); } const email = body.email.toLowerCase(); const { payload } = request; const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email } }, limit: 1 }); if (docs.length !== 1) { return new UserNotFoundAPIError(); } const userRecord = docs[0]; if (!userRecord.hashedPassword) { return new InvalidCredentials(); } const isVerified = await verifyPassword( body.password, userRecord.hashedPassword, userRecord.hashSalt, userRecord.hashIterations ); if (!isVerified) { return new InvalidCredentials(); } const collectionConfig = payload.config.collections.find( (collection) => collection.slug === internal.usersCollectionSlug ); if (!collectionConfig) { return new MissingCollection(); } const sessionID = collectionConfig?.auth.useSessions ? uuid2() : null; if (collectionConfig?.auth.useSessions) { const now = /* @__PURE__ */ new Date(); const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1e3; const expiresAt = new Date(now.getTime() + tokenExpInMs); const session = { id: sessionID, createdAt: now, expiresAt }; if (!userRecord["sessions"]?.length) { userRecord["sessions"] = [session]; } else { userRecord.sessions = removeExpiredSessions(userRecord.sessions); userRecord.sessions.push(session); } await payload.db.updateOne({ id: userRecord.id, collection: internal.usersCollectionSlug, data: userRecord, req: request, returning: false }); } const cookieName = useAdmin ? `${payload.config.cookiePrefix}-token` : `__${pluginType}-${APP_COOKIE_SUFFIX}`; const signinFields = { id: userRecord.id, email, sid: sessionID, collection: internal.usersCollectionSlug }; return await redirectWithSession( cookieName, successRedirectPath, secret, signinFields, request, useAdmin ? collectionConfig.auth.tokenExpiration : void 0 ); }; var PasswordSignup = async (pluginType, request, internal, useAdmin, secret, successRedirectPath, errorRedirectPath) => { const body = request.json && await request.json(); if (!body?.email || !body.password) { return new InvalidRequestBodyError(); } const email = body.email.toLowerCase(); const { payload } = request; const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email } }, limit: 1 }); if (docs.length > 0) { return new EmailAlreadyExistError(); } const { hash: hashedPassword, salt: hashSalt, iterations } = await hashPassword(body.password); const userRecord = await payload.create({ collection: internal.usersCollectionSlug, data: { email, hashedPassword, hashIterations: iterations, hashSalt, ...body.userInfo } }); if (body.allowAutoSignin) { const collectionConfig = payload.config.collections.find( (collection) => collection.slug === internal.usersCollectionSlug ); if (!collectionConfig) { return new MissingCollection(); } const sessionID = collectionConfig?.auth.useSessions ? uuid2() : null; if (collectionConfig?.auth.useSessions) { const now = /* @__PURE__ */ new Date(); const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1e3; const expiresAt = new Date(now.getTime() + tokenExpInMs); const session = { id: sessionID, createdAt: now, expiresAt }; if (!userRecord["sessions"]?.length) { userRecord["sessions"] = [session]; } else { userRecord.sessions = removeExpiredSessions(userRecord.sessions); userRecord.sessions.push(session); } await payload.db.updateOne({ id: userRecord.id, collection: internal.usersCollectionSlug, data: userRecord, req: request, returning: false }); } const cookieName = useAdmin ? `${payload.config.cookiePrefix}-token` : `__${pluginType}-${APP_COOKIE_SUFFIX}`; const signinFields = { id: userRecord.id, email, sid: sessionID, collection: internal.usersCollectionSlug }; return await redirectWithSession( cookieName, successRedirectPath, secret, signinFields, request, useAdmin ? collectionConfig.auth.tokenExpiration : void 0 ); } return Response.json( { message: "Signed up successfully", kind: "Created" /* Created */, isSuccess: true, isError: false }, { status: 201 } ); }; var ForgotPasswordInit = async (request, internal, emailTemplate) => { const { payload } = request; const body = request.json && await request.json(); if (!body?.email) { return new InvalidRequestBodyError(); } const email = body.email.toLowerCase(); const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email } }, limit: 1 }); if (docs.length !== 1) { return new UserNotFoundAPIError(); } const { code, hash } = await ephemeralCode(6, payload.secret); await payload.sendEmail({ to: email, subject: "Password recovery", html: await emailTemplate({ verificationCode: code }) }); const res = new Response( JSON.stringify({ message: "Verification email sent", kind: "Created" /* Created */, isSuccess: true, isError: false }), { status: 201 } ); const verification_token_expires = /* @__PURE__ */ new Date(); verification_token_expires.setDate(verification_token_expires.getDate() + 7); await payload.update({ collection: internal.usersCollectionSlug, id: docs[0].id, data: { verificationHash: hash, verificationCode: code, verificationTokenExpire: Math.floor( verification_token_expires.getTime() / 1e3 ), verificationKind: "PASSWORD_RESTORE" } }); return res; }; var ForgotPasswordVerify = async (request, internal) => { const { payload } = request; const body = request.json && await request.json(); if (!body?.password || !body.code) { return new InvalidRequestBodyError(); } const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { verificationCode: { equals: body.code } } }); const currentDate = Date.now(); if (docs.length === 0 || docs[0].verificationCode !== body.code || !docs[0].verificationHash || Math.floor(currentDate / 1e3) > docs[0].verificationTokenExpire || docs[0].verificationKind !== "PASSWORD_RESTORE") { return new MissingOrInvalidVerification(); } const { verificationHash: hash, id: userId } = docs[0]; const isVerified = await verifyEphemeralCode(body.code, hash, payload.secret); if (!isVerified) { return new MissingOrInvalidVerification(); } const { hash: hashedPassword, salt: hashSalt, iterations } = await hashPassword(body.password); await payload.update({ collection: internal.usersCollectionSlug, id: userId, data: { hashedPassword, hashSalt, hashIterations: iterations, verificationHash: null, verificationCode: null, verificationTokenExpire: null, verificationKind: null } }); const res = new Response( JSON.stringify({ message: "Password recovered successfully", kind: "Updated" /* Updated */, isSuccess: true, isError: false }), { status: 201 } ); return res; }; var ResetPassword = async (cookieName, secret, internal, request) => { const { payload } = request; const cookies = parseCookies6(request.headers); const token = cookies.get(cookieName); if (!token) { return new UnauthorizedAPIRequest(); } const jwtResponse = await verifySessionCookie(token, secret); if (!jwtResponse.payload) { return new UnauthorizedAPIRequest(); } const body = request.json && await request.json(); if (!body?.email || !body?.currentPassword || !body?.newPassword) { return new InvalidRequestBodyError(); } const email = body.email.toLowerCase(); const { docs } = await payload.find({ collection: internal.usersCollectionSlug, where: { email: { equals: email } }, limit: 1 }); if (docs.length !== 1) { return new UserNotFoundAPIError(); } const user = docs[0]; const isVerified = await verifyPassword( body.currentPassword, user.hashedPassword, user.hashSalt, user.hashIterations ); if (!isVerified) { return new InvalidCredentials(); } const { hash: hashedPassword, salt: hashSalt, iterations } = await hashPassword(body.newPassword); await payload.update({ collection: internal.usersCollectionSlug, id: user.id, data: { hashedPassword, hashSalt, hashIterations: iterations } }); const res = new Response( JSON.stringify({ message: "Password reset complete", kind: "Updated" /* Updated */, isSuccess: true, isError: false }), { status: 201 } ); return res; }; // src/core/routeHandlers/password.ts function PasswordAuthHandlers(request, pluginType, kind, internal, secret, useAdmin, successRedirectPath, errorRedirectPath, providerConfig, stage) { switch (kind) { case "signin": return PasswordSignin( pluginType, request, internal, useAdmin, secret, successRedirectPath, errorRedirectPath ); case "signup": return PasswordSignup( pluginType, request, internal, useAdmin, secret, successRedirectPath, errorRedirectPath ); case "forgot-password": switch (stage) { case "init": return ForgotPasswordInit( request, internal, providerConfig.emailTemplates.forgotPassword ); case "verify": return ForgotPasswordVerify(request, internal); default: throw new InvalidAPIRequest(); } case "reset-password": return ResetPassword( `__${pluginType}-${APP_COOKIE_SUFFIX}`, secret, internal, request ); default: throw new InvalidAPIRequest(); } } // src/core/protocols/session.ts import { parseCookies as parseCookies7 } from "payload"; var SessionRefresh = async (cookieName, request) => { const { payload } = request; const cookies = parseCookies7(request.headers); const token = cookies.get(cookieName); if (!token) { return new UnauthorizedAPIRequest(); } const jwtResponse = await verifySessionCookie(token, payload.secret); if (!jwtResponse.payload) { return new UnauthorizedAPIRequest(); } let refreshCookies = []; refreshCookies = [ ...await createSessionCookies( cookieName, payload.secret, jwtResponse.payload ) ]; const res = new Response( JSON.stringify({ message: "Session refreshed", kind: "Updated" /* Updated */, isSuccess: true, isError: false }), { status: 201 } ); for (const cookie of refreshCookies) { res.headers.append("Set-Cookie", cookie); } return res; }; var SessionUser = async (cookieName, request, internal, fields) => { const { payload } = request; const cookies = parseCookies7(request.headers); const token = cookies.get(cookieName); if (!token) { return new Response( JSON.stringify({ message: "Missing user session", kind: "NotAuthenticated" /* NotAuthenticated */, data: {}, isSuccess: false, isError: true }), { status: 403 } ); } const jwtResponse = await verifySessionCookie(token, payload.secret); if (!jwtResponse.payload) { return new Response( JSON.stringify({ message: "Invalid user session", kind: "NotAuthenticated" /* NotAuthenticated */, data: {}, isSuccess: false, isError: true }), { status: 401 } ); } const doc = await request.payload.findByID({ collection: internal.usersCollectionSlug, id: jwtResponse.payload.id }); if (!doc?.id) { return new UserNotFoundAPIError(); } return new Response( JSON.stringify({ message: "Fetched user session", kind: "Retrieved" /* Retrieved */, data: { isAuthenticated: true, user: { id: doc.id, email: doc.email } }, isSuccess: true, isError: false }), { status: 200 } ); }; var SessionSignout = async (cookieName, request) => { const searchParams = request.query; const expired = "Thu, 01 Jan 1970 00:00:00 GMT"; const cookies = []; cookies.push( `${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Expires=${expired}` ); let res = new Response( JSON.stringify({ message: "Signed Out", kind: "Deleted" /* Deleted */, isSuccess: true, isError: false }), { status: 200 } ); if (searchParams.returnTo) { const returnToURL = new URL(`${request.origin}/${searchParams.returnTo}`); res = new Response(null, { status: 302, headers: { Location: returnToURL.href } }); } for (const cookie of cookies) { res.headers.append("Set-Cookie", cookie); } return res; }; // src/core/rout