payload-auth-plugin
Version:
Authentication plugin for Payload CMS
326 lines (325 loc) • 10.4 kB
JavaScript
// src/core/protocols/password.ts
import { parseCookies } from "payload";
import {
EmailAlreadyExistError,
InvalidCredentials,
InvalidRequestBodyError,
MissingCollection,
MissingOrInvalidVerification,
UnauthorizedAPIRequest,
UserNotFoundAPIError
} from "../errors/apiErrors.js";
import { hashPassword, verifyPassword } from "../utils/password.js";
import { SuccessKind } from "../../types.js";
import { ephemeralCode, verifyEphemeralCode } from "../utils/hash.js";
import { APP_COOKIE_SUFFIX } from "../../constants.js";
import {
createSessionCookies,
invalidateOAuthCookies,
verifySessionCookie
} from "../utils/cookies.js";
import { v4 as uuid } from "uuid";
import { removeExpiredSessions } from "../utils/session.js";
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 isVerifed = await verifyPassword(body.password, userRecord.hashedPassword, userRecord.hashSalt, userRecord.hashIterations);
if (!isVerifed) {
return new InvalidCredentials;
}
const collectionConfig = payload.config.collections.find((collection) => collection.slug === internal.usersCollectionSlug);
if (!collectionConfig) {
return new MissingCollection;
}
const sessionID = collectionConfig?.auth.useSessions ? uuid() : null;
if (collectionConfig?.auth.useSessions) {
const now = new Date;
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000;
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 : undefined);
};
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 ? uuid() : null;
if (collectionConfig?.auth.useSessions) {
const now = new Date;
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000;
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 : undefined);
}
return Response.json({
message: "Signed up successfully",
kind: SuccessKind.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: SuccessKind.Created,
isSuccess: true,
isError: false
}), { status: 201 });
const verification_token_expires = 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() / 1000),
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 / 1000) > 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: SuccessKind.Updated,
isSuccess: true,
isError: false
}), { status: 201 });
return res;
};
var ResetPassword = async (cookieName, secret, internal, request) => {
const { payload } = request;
const cookies = parseCookies(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 isVerifed = await verifyPassword(body.currentPassword, user.hashedPassword, user.hashSalt, user.hashIterations);
if (!isVerifed) {
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: SuccessKind.Updated,
isSuccess: true,
isError: false
}), {
status: 201
});
return res;
};
export {
ResetPassword,
PasswordSignup,
PasswordSignin,
ForgotPasswordVerify,
ForgotPasswordInit
};