swaf/src/auth/models/MagicLink.ts

108 lines
3.4 KiB
TypeScript

import argon2 from "argon2";
import config from "config";
import crypto from "crypto";
import Model from "../../db/Model.js";
import {EMAIL_REGEX} from "../../db/Validator.js";
import AuthProof from "../AuthProof.js";
import User from "./User.js";
import UserEmail from "./UserEmail.js";
export default class MagicLink extends Model implements AuthProof<User> {
public static validityPeriod(): number {
return config.get<number>('magic_link.validity_period') * 1000;
}
public readonly id?: number = undefined;
public readonly session_id?: string = undefined;
private email?: string = undefined;
private token?: string = undefined;
public readonly action_type?: string = undefined;
public readonly original_url?: string = undefined;
private generated_at?: Date = undefined;
private authorized: boolean = false;
private used: boolean = false;
protected init(): void {
this.setValidation('session_id').defined().length(32);
this.setValidation('email').defined().regexp(EMAIL_REGEX);
this.setValidation('token').defined().length(96);
this.setValidation('action_type').defined().maxLength(64);
this.setValidation('original_url').defined().maxLength(1745);
this.setValidation('authorized').defined();
this.setValidation('used').defined();
}
public async getResource(): Promise<User | null> {
const email = await UserEmail.select()
.with('user')
.where('email', await this.getOrFail('email'))
.first();
return email ? await email.user.get() : null;
}
public async revoke(): Promise<void> {
await this.delete();
}
public async isValid(): Promise<boolean> {
return await this.isAuthorized() ||
new Date().getTime() < this.getExpirationDate().getTime();
}
public async isAuthorized(): Promise<boolean> {
return this.authorized;
}
public authorize(): void {
this.authorized = true;
}
public isUsed(): boolean {
return this.used;
}
public useLink(): void {
this.used = true;
}
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,
hashLength: 32,
});
return rawToken;
}
/**
* @returns {@code null} if the token is valid, an error {@code string} otherwise.
*/
public async verifyToken(tokenGuess: string): Promise<string | null> {
// There is no token
if (this.token === undefined || this.generated_at === undefined)
return 'This token was not generated.';
// Token has expired
if (new Date().getTime() - this.generated_at.getTime() > MagicLink.validityPeriod())
return 'This token has expired.';
// Token is invalid
if (!await argon2.verify(this.token, tokenGuess))
return 'This token is invalid.';
return null;
}
public getExpirationDate(): Date {
if (!this.generated_at) return new Date();
return new Date(this.generated_at.getTime() + MagicLink.validityPeriod());
}
}