2020-04-24 12:12:27 +02:00
|
|
|
import AuthProof from "./AuthProof";
|
|
|
|
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
|
|
|
import User from "./models/User";
|
2020-04-25 09:35:49 +02:00
|
|
|
import {Connection} from "mysql";
|
2020-11-11 19:08:33 +01:00
|
|
|
import {Request, Response} from "express";
|
2020-07-20 17:32:32 +02:00
|
|
|
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
|
2020-11-14 17:24:42 +01:00
|
|
|
import Mail from "../mail/Mail";
|
2020-07-20 17:32:32 +02:00
|
|
|
import Controller from "../Controller";
|
|
|
|
import config from "config";
|
2020-11-03 10:29:36 +01:00
|
|
|
import Application from "../Application";
|
|
|
|
import NunjucksComponent from "../components/NunjucksComponent";
|
2020-11-11 19:08:33 +01:00
|
|
|
import AuthMethod from "./AuthMethod";
|
2020-12-04 14:42:09 +01:00
|
|
|
import {Session, SessionData} from "express-session";
|
2021-01-25 14:37:50 +01:00
|
|
|
import UserNameComponent from "./models/UserNameComponent";
|
2020-11-11 19:08:33 +01:00
|
|
|
|
|
|
|
export default class AuthGuard {
|
|
|
|
private readonly authMethods: AuthMethod<AuthProof<User>>[];
|
2020-04-24 12:12:27 +02:00
|
|
|
|
2020-11-03 10:29:36 +01:00
|
|
|
public constructor(
|
|
|
|
private readonly app: Application,
|
2020-11-11 19:08:33 +01:00
|
|
|
...authMethods: AuthMethod<AuthProof<User>>[]
|
2020-11-03 10:29:36 +01:00
|
|
|
) {
|
2020-11-11 19:08:33 +01:00
|
|
|
this.authMethods = authMethods;
|
2020-11-03 10:29:36 +01:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
public async interruptAuth(req: Request, res: Response): Promise<boolean> {
|
|
|
|
for (const method of this.authMethods) {
|
|
|
|
if (method.interruptAuth && await method.interruptAuth(req, res)) return true;
|
|
|
|
}
|
2020-04-24 12:12:27 +02:00
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
return false;
|
2020-06-14 11:59:02 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
public getAuthMethodByName(authMethodName: string): AuthMethod<AuthProof<User>> | null {
|
|
|
|
return this.authMethods.find(m => m.getName() === authMethodName) || null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getAuthMethodNames(): string[] {
|
|
|
|
return this.authMethods.map(m => m.getName());
|
|
|
|
}
|
|
|
|
|
|
|
|
public getRegistrationMethod(): AuthMethod<AuthProof<User>> {
|
|
|
|
return this.authMethods[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getAuthMethodsByIdentifier(
|
|
|
|
identifier: string,
|
|
|
|
): Promise<{ user: User, method: AuthMethod<AuthProof<User>> }[]> {
|
|
|
|
const methods = [];
|
|
|
|
for (const method of this.authMethods) {
|
|
|
|
const user = await method.findUserByIdentifier(identifier);
|
|
|
|
if (user) methods.push({user, method});
|
2020-07-28 11:47:20 +02:00
|
|
|
}
|
2020-11-11 19:08:33 +01:00
|
|
|
return methods;
|
2020-07-28 11:47:20 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
public async getProofs(req: Request): Promise<AuthProof<User>[]> {
|
|
|
|
const proofs = [];
|
2020-12-04 14:42:09 +01:00
|
|
|
if (req.getSessionOptional()) {
|
2020-11-11 19:08:33 +01:00
|
|
|
proofs.push(...await this.getProofsForSession(req.session));
|
|
|
|
}
|
|
|
|
proofs.push(...await this.getProofsForRequest(req));
|
|
|
|
return proofs;
|
|
|
|
}
|
2020-07-28 11:47:20 +02:00
|
|
|
|
2020-12-04 14:42:09 +01:00
|
|
|
public async getProofsForSession(session: Session & Partial<SessionData>): Promise<AuthProof<User>[]> {
|
2021-01-25 10:53:43 +01:00
|
|
|
if (!session.isAuthenticated) return [];
|
2020-11-11 19:08:33 +01:00
|
|
|
|
|
|
|
const proofs = [];
|
|
|
|
for (const method of this.authMethods) {
|
|
|
|
if (method.getProofsForSession) {
|
|
|
|
const methodProofs = await method.getProofsForSession(session);
|
|
|
|
for (const proof of methodProofs) {
|
|
|
|
if (!await proof.isValid() || !await proof.isAuthorized()) {
|
|
|
|
await proof.revoke();
|
|
|
|
} else {
|
|
|
|
proofs.push(proof);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-28 11:47:20 +02:00
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
if (proofs.length === 0) {
|
2021-01-25 10:53:43 +01:00
|
|
|
session.isAuthenticated = false;
|
2021-01-24 16:29:23 +01:00
|
|
|
session.persistent = false;
|
2020-07-28 11:47:20 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
return proofs;
|
2020-07-28 11:47:20 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
public async getProofsForRequest(req: Request): Promise<AuthProof<User>[]> {
|
|
|
|
const proofs = [];
|
|
|
|
for (const method of this.authMethods) {
|
|
|
|
if (method.getProofsForRequest) {
|
|
|
|
const methodProofs = await method.getProofsForRequest(req);
|
|
|
|
for (const proof of methodProofs) {
|
|
|
|
if (!await proof.isValid() || !await proof.isAuthorized()) {
|
|
|
|
await proof.revoke();
|
|
|
|
} else {
|
|
|
|
proofs.push(proof);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-28 11:47:20 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:08:33 +01:00
|
|
|
return proofs;
|
2020-07-28 11:47:20 +02:00
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
public async authenticateOrRegister(
|
2020-12-04 14:42:09 +01:00
|
|
|
session: Session & Partial<SessionData>,
|
2020-11-11 19:08:33 +01:00
|
|
|
proof: AuthProof<User>,
|
2021-01-24 16:29:23 +01:00
|
|
|
persistSession: boolean,
|
2020-07-25 10:28:50 +02:00
|
|
|
onLogin?: (user: User) => Promise<void>,
|
2020-11-04 11:55:34 +01:00
|
|
|
beforeRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
|
|
|
afterRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
2020-09-25 22:03:22 +02:00
|
|
|
): Promise<User> {
|
2020-07-25 10:28:50 +02:00
|
|
|
if (!await proof.isValid()) throw new InvalidAuthProofError();
|
|
|
|
if (!await proof.isAuthorized()) throw new UnauthorizedAuthProofError();
|
2020-04-24 12:12:27 +02:00
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
let user = await proof.getResource();
|
2020-04-24 12:12:27 +02:00
|
|
|
|
2021-04-22 15:38:24 +02:00
|
|
|
// Revoke proof early if user is not approved
|
|
|
|
if (user && !user.isApproved() || !user && User.isApprovalMode()) {
|
|
|
|
await proof.revoke();
|
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
// Register if user doesn't exist
|
|
|
|
if (!user) {
|
|
|
|
const callbacks: RegisterCallback[] = [];
|
2020-04-24 12:12:27 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
user = await MysqlConnectionManager.wrapTransaction(async connection => {
|
|
|
|
const user = User.create({});
|
2020-11-04 11:55:34 +01:00
|
|
|
if (beforeRegister) {
|
|
|
|
(await beforeRegister(connection, user)).forEach(c => callbacks.push(c));
|
|
|
|
}
|
2020-08-26 14:03:41 +02:00
|
|
|
await user.save(connection, c => callbacks.push(c));
|
2020-11-04 11:55:34 +01:00
|
|
|
if (afterRegister) {
|
|
|
|
(await afterRegister(connection, user)).forEach(c => callbacks.push(c));
|
2020-04-25 09:36:20 +02:00
|
|
|
}
|
2020-09-25 23:42:15 +02:00
|
|
|
return user;
|
2020-04-24 12:12:27 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
for (const callback of callbacks) {
|
|
|
|
await callback();
|
|
|
|
}
|
|
|
|
|
2021-04-22 15:38:24 +02:00
|
|
|
if (User.isApprovalMode()) {
|
2020-11-03 10:29:36 +01:00
|
|
|
await new Mail(this.app.as(NunjucksComponent).getEnvironment(), PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
2021-02-23 17:42:25 +01:00
|
|
|
username: user.asOptional(UserNameComponent)?.getName() ||
|
2021-01-25 14:37:50 +01:00
|
|
|
(await user.mainEmail.get())?.getOrFail('email') ||
|
|
|
|
'Could not find an identifier',
|
2021-01-21 17:12:59 +01:00
|
|
|
link: config.get<string>('public_url') + Controller.route('accounts-approval'),
|
2020-09-25 23:42:15 +02:00
|
|
|
}).send(config.get<string>('app.contact_email'));
|
2020-04-24 12:12:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
// Don't login if user isn't approved
|
2020-07-24 13:00:20 +02:00
|
|
|
if (!user.isApproved()) {
|
|
|
|
throw new PendingApprovalAuthError();
|
|
|
|
}
|
2020-06-16 11:12:58 +02:00
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
// Login
|
2021-01-25 10:53:43 +01:00
|
|
|
session.isAuthenticated = true;
|
2021-01-24 16:29:23 +01:00
|
|
|
session.persistent = persistSession;
|
2020-07-25 10:28:50 +02:00
|
|
|
if (onLogin) await onLogin(user);
|
2020-09-25 22:03:22 +02:00
|
|
|
|
|
|
|
return user;
|
2020-04-24 12:12:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class AuthError extends Error {
|
2020-04-25 16:08:20 +02:00
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
export class AuthProofError extends AuthError {
|
|
|
|
}
|
|
|
|
|
|
|
|
export class InvalidAuthProofError extends AuthProofError {
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor() {
|
2020-07-25 10:28:50 +02:00
|
|
|
super('Invalid auth proof.');
|
|
|
|
}
|
|
|
|
}
|
2020-04-25 16:08:20 +02:00
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
export class UnauthorizedAuthProofError extends AuthProofError {
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor() {
|
2020-07-25 10:28:50 +02:00
|
|
|
super('Unauthorized auth proof.');
|
2020-04-25 16:08:20 +02:00
|
|
|
}
|
2020-06-16 11:12:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export class PendingApprovalAuthError extends AuthError {
|
2020-09-25 23:42:15 +02:00
|
|
|
public constructor() {
|
2020-06-16 11:12:58 +02:00
|
|
|
super(`User is not approved.`);
|
|
|
|
}
|
2020-07-24 13:00:20 +02:00
|
|
|
}
|
2020-07-25 10:28:50 +02:00
|
|
|
|
|
|
|
export type RegisterCallback = () => Promise<void>;
|