swaf/src/auth/models/MagicLink.ts

126 lines
4.3 KiB
TypeScript

import crypto from "crypto";
import config from "config";
import Model, {EMAIL_REGEX} from "../../db/Model";
import AuthProof from "../AuthProof";
import Validator from "../../db/Validator";
import User from "./User";
import argon2 from "argon2";
export default class MagicLink extends Model implements AuthProof {
public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise<MagicLink | null> {
let query = this.select().where('session_id', sessionID);
if (actionType !== undefined) {
if (typeof actionType === 'string') {
query = query.where('action_type', actionType);
} else {
query = query.whereIn('action_type', actionType);
}
}
const links = await this.models<MagicLink>(query.first());
return links.length > 0 ? links[0] : null;
}
public static validityPeriod(): number {
return config.get<number>('magic_link.validity_period') * 1000;
}
private session_id?: string;
private email?: string;
private token?: string;
private action_type?: string;
private original_url?: string;
private generated_at?: Date;
private authorized?: boolean;
constructor(data: any) {
super(data);
if (this.action_type === undefined) throw new Error('Action type must always be defined.');
if (this.original_url === undefined) throw new Error('Origin url must always be defined.');
if (this.authorized === undefined) {
this.authorized = false;
}
}
protected defineProperties(): void {
this.defineProperty<string>('session_id', new Validator().defined().length(32).unique(this));
this.defineProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX));
this.defineProperty<string>('token', new Validator().defined().length(96));
this.defineProperty<string>('action_type', new Validator().defined().maxLength(64));
this.defineProperty<string>('original_url', new Validator().defined().maxLength(1745));
this.defineProperty<Date>('generated_at', new Validator());
this.defineProperty<boolean>('authorized', new Validator().defined());
}
public async isOwnedBy(userId: number): Promise<boolean> {
const user = await this.getUser();
return user !== null && user.id === userId;
}
public async getUser(): Promise<User | null> {
return await User.fromEmail(await this.getEmail());
}
public async revoke(): Promise<void> {
await this.delete();
}
public async isValid(): Promise<boolean> {
if (await this.isAuthorized()) return true;
return new Date().getTime() < this.getExpirationDate().getTime();
}
public async isAuthorized(): Promise<boolean> {
return this.authorized!;
}
public async generateToken(email: string): Promise<string> {
const rawToken = crypto.randomBytes(48).toString('base64'); // Raw token length = 64
this.email = email;
this.generated_at = new Date();
this.token = await argon2.hash(rawToken, {
timeCost: 10,
memoryCost: 4096,
parallelism: 4
});
return rawToken;
}
/**
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
*/
public async verifyToken(tokenGuess: string): Promise<string | null> {
if (this.token === undefined || this.generated_at === undefined) return 'This token was not generated.'; // There is no token
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod()) return 'This token has expired.'; // Token has expired
if (!await argon2.verify(this.token, tokenGuess)) return 'This token is invalid.';
return null;
}
public getSessionID(): string {
return this.session_id!;
}
public async getEmail(): Promise<string> {
return this.email!;
}
public getActionType(): string {
return this.action_type!;
}
public getOriginalURL(): string {
return this.original_url!;
}
public getExpirationDate(): Date {
if (!this.generated_at) return new Date();
return new Date(this.generated_at?.getTime() + MagicLink.validityPeriod());
}
public authorize() {
this.authorized = true;
}
}