swaf/src/auth/password/PasswordAuthMethod.ts

141 lines
5.5 KiB
TypeScript

import {Request, Response} from "express";
import {Session} from "express-session";
import Application from "../../Application.js";
import {route} from "../../common/Routing.js";
import ModelFactory from "../../db/ModelFactory.js";
import Validator, {InvalidFormatValidationError} from "../../db/Validator.js";
import {ServerError} from "../../HttpError.js";
import Throttler from "../../Throttler.js";
import AuthComponent from "../AuthComponent.js";
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard.js";
import AuthMethod from "../AuthMethod.js";
import User from "../models/User.js";
import UserEmail from "../models/UserEmail.js";
import UserNameComponent, {USERNAME_REGEXP} from "../models/UserNameComponent.js";
import PasswordAuthProof from "./PasswordAuthProof.js";
import UserPasswordComponent from "./UserPasswordComponent.js";
export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof> {
public constructor(
protected readonly app: Application,
) {
}
public getName(): string {
return 'password';
}
public getWeightForRequest(req: Request): number {
return !req.body.identifier || !req.body.password || req.body.password.length === 0 ?
0 :
2;
}
public async findUserByIdentifier(identifier: string): Promise<User | null> {
const query = UserEmail.select()
.with('user')
.where('email', identifier);
const user = (await query
.first())?.user.getOrFail();
if (user) return user;
if (ModelFactory.get(User).hasComponent(UserNameComponent)) {
return await User.select().where('name', identifier).first();
}
return null;
}
public async getProofsForSession(session: Session): Promise<PasswordAuthProof[]> {
const proof = PasswordAuthProof.getProofForSession(session);
return proof ? [proof] : [];
}
public async attemptLogin(req: Request, res: Response, user: User): Promise<void> {
const passwordAuthProof = PasswordAuthProof.createProofForLogin(req.getSession());
passwordAuthProof.setResource(user);
await passwordAuthProof.authorize(req.body.password);
try {
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(
req.getSession(),
passwordAuthProof,
!!req.body.persist_session,
);
} catch (e) {
if (e instanceof AuthError) {
Throttler.throttle('login_failed_attempts_user', 3, 3 * 60 * 1000, // 3min
<string>user.getOrFail('name'), 1000, 60 * 1000); // 1min
Throttler.throttle('login_failed_attempts_ip', 50, 60 * 1000, // 1min
req.ip, 1000, 3600 * 1000); // 1h
if (e instanceof PendingApprovalAuthError) {
req.flash('error', 'Your account is still being reviewed.');
res.redirect(route('auth'));
return;
} else {
const err = new InvalidFormatValidationError('Invalid password.');
err.thingName = 'password';
throw err;
}
} else {
throw e;
}
}
req.flash('success', `Welcome, ${user.name}.`);
res.redirect(req.getIntendedUrl() || route('home'));
}
public async attemptRegister(req: Request, res: Response, identifier: string): Promise<void> {
if (!ModelFactory.get(User).hasComponent(UserNameComponent))
throw new ServerError('Cannot register with password without UserNameComponent.');
Throttler.throttle('register_password', 10, 30000, req.ip);
req.body.identifier = identifier;
await Validator.validate({
identifier: new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(User, 'name'),
password: new Validator().defined().minLength(UserPasswordComponent.PASSWORD_MIN_LENGTH),
password_confirmation: new Validator().defined().sameAs('password', req.body.password),
terms: new Validator().defined(),
}, req.body);
const passwordAuthProof = PasswordAuthProof.createAuthorizedProofForRegistration(req.getSession());
try {
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(req.getSession(), passwordAuthProof,
true, undefined, async (connection, user) => {
const callbacks: RegisterCallback[] = [];
// Password
await user.as(UserPasswordComponent).setPassword(req.body.password);
// Username
user.as(UserNameComponent).setName(req.body.identifier);
return callbacks;
}, async (connection, user) => {
passwordAuthProof.setResource(user);
return [];
});
} catch (e) {
if (e instanceof PendingApprovalAuthError) {
req.flash('info', `Your account was successfully created and is pending review from an administrator.`);
res.redirect(route('auth'));
return;
} else {
throw e;
}
}
const user = await passwordAuthProof.getResource();
req.flash('success', `Your account was successfully created! Welcome, ${user?.as(UserNameComponent).getName()}.`);
res.redirect(req.getIntendedUrl() || route('home'));
}
}