payload-auth-plugin
Version:
Authentication plugin for Payload CMS
1,709 lines (1,681 loc) • 58 kB
JavaScript
// 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