chore: remove passport local mongoose (#2713)
This commit is contained in:
committed by
Dan Ribbens
parent
863be3d852
commit
03b1ee0896
@@ -4,7 +4,7 @@ import { extractTranslations } from '../../translations/extractTranslations';
|
||||
|
||||
const labels = extractTranslations(['general:email']);
|
||||
|
||||
export default [
|
||||
const baseAuthFields: Field[] = [
|
||||
{
|
||||
name: 'email',
|
||||
label: labels['general:email'],
|
||||
@@ -27,4 +27,16 @@ export default [
|
||||
type: 'date',
|
||||
hidden: true,
|
||||
},
|
||||
] as Field[];
|
||||
{
|
||||
name: 'salt',
|
||||
type: 'text',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'hash',
|
||||
type: 'text',
|
||||
hidden: true,
|
||||
}
|
||||
];
|
||||
|
||||
export default baseAuthFields
|
||||
|
||||
@@ -11,6 +11,8 @@ import { User } from '../types';
|
||||
import { Collection } from '../../collections/config/types';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import unlock from './unlock';
|
||||
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts';
|
||||
import { authenticateLocalStrategy } from '../strategies/local/authenticate';
|
||||
|
||||
export type Result = {
|
||||
user?: User,
|
||||
@@ -77,7 +79,9 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Improper typing in library, additional args should be optional
|
||||
const userDoc = await Model.findByUsername(email);
|
||||
const userDoc = await Model.findOne({ email }).lean();
|
||||
|
||||
let user = JSON.parse(JSON.stringify(userDoc));
|
||||
|
||||
if (!userDoc || (args.collection.config.auth.verify && userDoc._verified === false)) {
|
||||
throw new AuthenticationError(req.t);
|
||||
@@ -87,12 +91,21 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
throw new LockedAuth(req.t);
|
||||
}
|
||||
|
||||
const authResult = await userDoc.authenticate(password);
|
||||
const authResult = await authenticateLocalStrategy({ password, doc: user });
|
||||
|
||||
user = sanitizeInternalFields(user);
|
||||
|
||||
const maxLoginAttemptsEnabled = args.collection.config.auth.maxLoginAttempts > 0;
|
||||
|
||||
if (!authResult.user) {
|
||||
if (maxLoginAttemptsEnabled) await userDoc.incLoginAttempts();
|
||||
if (!authResult) {
|
||||
if (maxLoginAttemptsEnabled) {
|
||||
await incrementLoginAttempts({
|
||||
payload: req.payload,
|
||||
doc: user,
|
||||
collection: collectionConfig,
|
||||
});
|
||||
}
|
||||
|
||||
throw new AuthenticationError(req.t);
|
||||
}
|
||||
|
||||
@@ -108,10 +121,6 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
});
|
||||
}
|
||||
|
||||
let user = userDoc.toJSON({ virtuals: true });
|
||||
user = JSON.parse(JSON.stringify(user));
|
||||
user = sanitizeInternalFields(user);
|
||||
|
||||
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field: Field) => {
|
||||
const result = {
|
||||
...signedFields,
|
||||
|
||||
@@ -6,6 +6,8 @@ import getCookieExpiration from '../../utilities/getCookieExpiration';
|
||||
import { UserDocument } from '../types';
|
||||
import { fieldAffectsData } from '../../fields/config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { authenticateLocalStrategy } from '../strategies/local/authenticate';
|
||||
import { generatePasswordSaltHash } from '../strategies/local/generatePasswordSaltHash';
|
||||
|
||||
export type Result = {
|
||||
token: string
|
||||
@@ -49,14 +51,20 @@ async function resetPassword(args: Arguments): Promise<Result> {
|
||||
// Reset Password
|
||||
// /////////////////////////////////////
|
||||
|
||||
const user = await Model.findOne({
|
||||
let user = await Model.findOne({
|
||||
resetPasswordToken: data.token,
|
||||
resetPasswordExpiration: { $gt: Date.now() },
|
||||
}) as UserDocument;
|
||||
}).lean();
|
||||
|
||||
user = JSON.parse(JSON.stringify(user));
|
||||
|
||||
if (!user) throw new APIError('Token is either invalid or has expired.');
|
||||
|
||||
await user.setPassword(data.password);
|
||||
// TODO: replace this method
|
||||
const { salt, hash } = await generatePasswordSaltHash({ password: data.password })
|
||||
|
||||
user.salt = salt;
|
||||
user.hash = hash;
|
||||
|
||||
user.resetPasswordExpiration = Date.now();
|
||||
|
||||
@@ -64,9 +72,11 @@ async function resetPassword(args: Arguments): Promise<Result> {
|
||||
user._verified = true;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
let doc = await Model.findByIdAndUpdate({ _id: user.id }, user, { new: true }).lean()
|
||||
|
||||
await user.authenticate(data.password);
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
await authenticateLocalStrategy({ password: data.password, doc });
|
||||
|
||||
const fieldsToSign = collectionConfig.fields.reduce((signedFields, field) => {
|
||||
if (fieldAffectsData(field) && field.saveToJWT) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { APIError } from '../../errors';
|
||||
import executeAccess from '../executeAccess';
|
||||
import { Collection } from '../../collections/config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts';
|
||||
|
||||
export type Args = {
|
||||
collection: Collection
|
||||
@@ -46,7 +47,11 @@ async function unlock(args: Args): Promise<boolean> {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
await user.resetLoginAttempts();
|
||||
await resetLoginAttempts({
|
||||
payload: req.payload,
|
||||
collection: collectionConfig,
|
||||
doc: user,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
41
src/auth/strategies/local/authenticate.ts
Normal file
41
src/auth/strategies/local/authenticate.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import crypto from 'crypto'
|
||||
import scmp from 'scmp'
|
||||
import { TypeWithID } from "../../../collections/config/types"
|
||||
import { AuthenticationError } from "../../../errors"
|
||||
|
||||
type Doc = TypeWithID & Record<string, unknown>
|
||||
|
||||
type Args = {
|
||||
doc: Doc
|
||||
password: string
|
||||
}
|
||||
|
||||
export const authenticateLocalStrategy = async ({
|
||||
doc,
|
||||
password,
|
||||
}: Args): Promise<Doc | null> => {
|
||||
try {
|
||||
const salt = doc.salt
|
||||
const hash = doc.hash
|
||||
|
||||
if (typeof salt === 'string' && typeof hash === 'string') {
|
||||
const res = await new Promise<Doc | null>((resolve, reject) => {
|
||||
crypto.pbkdf2(password, salt, 25000, 512, 'sha256', (e, hashBuffer) => {
|
||||
if (e) reject(null)
|
||||
|
||||
if (scmp(hashBuffer, Buffer.from(hash, 'hex'))) {
|
||||
resolve(doc)
|
||||
} else {
|
||||
reject(null)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
39
src/auth/strategies/local/generatePasswordSaltHash.ts
Normal file
39
src/auth/strategies/local/generatePasswordSaltHash.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import crypto from 'crypto'
|
||||
import { ValidationError } from "../../../errors"
|
||||
|
||||
const defaultPasswordValidator = (password: string): string | true => {
|
||||
if (!password) return 'No password was given'
|
||||
if (password.length < 3) return 'Password must be at least 3 characters'
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function randomBytes(): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => crypto.randomBytes(32, (err, saltBuffer) => (err ? reject(err) : resolve(saltBuffer))));
|
||||
}
|
||||
|
||||
function pbkdf2Promisified(password: string, salt: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => crypto.pbkdf2(password, salt, 25000, 512, 'sha256', (err, hashRaw) => (err ? reject(err) : resolve(hashRaw))));
|
||||
}
|
||||
|
||||
type Args = {
|
||||
password: string
|
||||
}
|
||||
|
||||
export const generatePasswordSaltHash = async ({
|
||||
password,
|
||||
}: Args): Promise<{ salt: string, hash: string }> => {
|
||||
const validationResult = defaultPasswordValidator(password)
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
throw new ValidationError([{ message: validationResult, field: 'password' }])
|
||||
}
|
||||
|
||||
const saltBuffer = await randomBytes()
|
||||
const salt = saltBuffer.toString('hex')
|
||||
|
||||
const hashRaw = await pbkdf2Promisified(password, salt)
|
||||
const hash = hashRaw.toString('hex')
|
||||
|
||||
return { salt, hash }
|
||||
}
|
||||
56
src/auth/strategies/local/incrementLoginAttempts.ts
Normal file
56
src/auth/strategies/local/incrementLoginAttempts.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Payload } from "../../.."
|
||||
import { SanitizedCollectionConfig, TypeWithID } from "../../../collections/config/types"
|
||||
|
||||
type Args = {
|
||||
payload: Payload
|
||||
doc: TypeWithID & Record<string, unknown>
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
export const incrementLoginAttempts = async ({
|
||||
payload,
|
||||
doc,
|
||||
collection,
|
||||
}: Args): Promise<void> => {
|
||||
const {
|
||||
auth: {
|
||||
maxLoginAttempts,
|
||||
lockTime,
|
||||
}
|
||||
} = collection
|
||||
|
||||
if ('lockUntil' in doc && typeof doc.lockUntil === 'string') {
|
||||
const lockUntil = Math.floor(new Date(doc.lockUntil).getTime() / 1000)
|
||||
|
||||
// Expired lock, restart count at 1
|
||||
if (lockUntil < Date.now()) {
|
||||
await payload.update({
|
||||
collection: collection.slug,
|
||||
id: doc.id,
|
||||
data: {
|
||||
loginAttempts: 1,
|
||||
lockUntil: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
loginAttempts: Number(doc.loginAttempts) + 1,
|
||||
}
|
||||
|
||||
// Lock the account if at max attempts and not already locked
|
||||
if (typeof doc.loginAttempts === 'number' && doc.loginAttempts + 1 >= maxLoginAttempts) {
|
||||
const lockUntil = new Date((Date.now() + lockTime))
|
||||
data.lockUntil = lockUntil
|
||||
}
|
||||
|
||||
|
||||
await payload.update({
|
||||
collection: collection.slug,
|
||||
id: doc.id,
|
||||
data,
|
||||
})
|
||||
}
|
||||
43
src/auth/strategies/local/register.ts
Normal file
43
src/auth/strategies/local/register.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ValidationError } from "../../../errors"
|
||||
import { Payload } from '../../..'
|
||||
import { SanitizedCollectionConfig } from '../../../collections/config/types'
|
||||
import { generatePasswordSaltHash } from './generatePasswordSaltHash';
|
||||
|
||||
type Args = {
|
||||
collection: SanitizedCollectionConfig
|
||||
doc: Record<string, unknown>
|
||||
password: string
|
||||
payload: Payload
|
||||
}
|
||||
|
||||
export const registerLocalStrategy = async ({
|
||||
collection,
|
||||
doc,
|
||||
password,
|
||||
payload,
|
||||
}: Args): Promise<Record<string, unknown>> => {
|
||||
const existingUser = await payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
where: {
|
||||
email: {
|
||||
equals: doc.email,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (existingUser.docs.length > 0) {
|
||||
throw new ValidationError([{ message: 'A user with the given email is already registered', field: 'email' }])
|
||||
}
|
||||
|
||||
const { salt, hash } = await generatePasswordSaltHash({ password })
|
||||
|
||||
const result = await payload.collections[collection.slug].Model.create({
|
||||
...doc,
|
||||
salt,
|
||||
hash
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
23
src/auth/strategies/local/resetLoginAttempts.ts
Normal file
23
src/auth/strategies/local/resetLoginAttempts.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Payload } from "../../.."
|
||||
import { SanitizedCollectionConfig, TypeWithID } from "../../../collections/config/types"
|
||||
|
||||
type Args = {
|
||||
payload: Payload
|
||||
doc: TypeWithID & Record<string, unknown>
|
||||
collection: SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
export const resetLoginAttempts = async ({
|
||||
payload,
|
||||
doc,
|
||||
collection,
|
||||
}: Args): Promise<void> => {
|
||||
await payload.update({
|
||||
collection: collection.slug,
|
||||
id: doc.id,
|
||||
data: {
|
||||
loginAttempts: 0,
|
||||
lockUntil: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user