Turn UserPassword and Username into a User model component

This commit is contained in:
Alice Gaudon 2020-07-25 10:53:54 +02:00
parent 3b8dee7fdf
commit 933d8f1cbb
11 changed files with 184 additions and 215 deletions

View File

@ -17,19 +17,25 @@ import HomeController from "./controllers/HomeController";
import AuthController from "./controllers/AuthController";
import AuthComponent from "wms-core/auth/AuthComponent";
import AuthGuard from "wms-core/auth/AuthGuard";
import {PasswordAuthProof} from "./models/UserPassword";
import {PasswordAuthProof} from "./models/UserPasswordComponent";
import LDAPServerComponent from "./LDAPServerComponent";
import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent";
import AccountController from "./controllers/AccountController";
import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable";
import CreateLogsTable from "wms-core/migrations/CreateLogsTable";
import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable";
import CreateUserPasswordsTable from "./migrations/CreateUserPasswordsTable";
import CreateUsernamesTable from "./migrations/CreateUsernamesTable";
import AddPasswordToUsers from "./migrations/AddPasswordToUsers";
import CreateUsernamesTable from "./migrations/AddNameToUsers";
import CreateMagicLinksTable from "wms-core/auth/migrations/CreateMagicLinksTable";
import MailController from "wms-core/auth/MailController";
import MagicLinkController from "./controllers/MagicLinkController";
import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener";
import BackendController from "wms-core/helpers/BackendController";
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
import FixUserMainEmailRelation from "wms-core/auth/migrations/FixUserMainEmailRelation";
import DropNameFromUsers from "wms-core/auth/migrations/DropNameFromUsers";
import MagicLink from "wms-core/auth/models/MagicLink";
import AddNameToUsers from "./migrations/AddNameToUsers";
export default class App extends Application {
private readonly port: number;
@ -45,9 +51,13 @@ export default class App extends Application {
CreateMigrationsTable,
CreateLogsTable,
CreateUsersAndUserEmailsTable,
CreateUserPasswordsTable,
AddPasswordToUsers,
CreateUsernamesTable,
CreateMagicLinksTable,
AddApprovedFieldToUsersTable,
FixUserMainEmailRelation,
DropNameFromUsers,
AddNameToUsers,
];
}
@ -91,9 +101,10 @@ export default class App extends Application {
this.use(new CsrfProtectionComponent());
// Auth
this.use(new AuthComponent(new class extends AuthGuard<PasswordAuthProof> {
public async getProofForSession(session: Express.Session): Promise<any | null> {
return PasswordAuthProof.getProofForSession(session);
this.use(new AuthComponent(new class extends AuthGuard<PasswordAuthProof | MagicLink> {
public async getProofForSession(session: Express.Session): Promise<PasswordAuthProof | MagicLink | null> {
return PasswordAuthProof.getProofForSession(session) ||
MagicLink.bySessionID(session.id);
}
}));
@ -111,9 +122,10 @@ export default class App extends Application {
private registerControllers() {
// Priority routes / interrupting middlewares
this.use(new AuthController());
this.use(new AccountController());
this.use(new MagicLinkController(this.magicLinkWebSocketListener!))
this.use(new BackendController());
this.use(new AuthController());
// Core functionality
this.use(new MailController());

View File

@ -3,7 +3,7 @@ import {Express} from "express";
import ldap, {InvalidCredentialsError, Server} from "ldapjs";
import Logger from "wms-core/Logger";
import Username from "./models/Username";
import {PasswordAuthProof} from "./models/UserPassword";
import {PasswordAuthProof} from "./models/UserPasswordComponent";
import Throttler from "wms-core/Throttler";
export default class LDAPServerComponent extends ApplicationComponent<void> {

View File

@ -0,0 +1,20 @@
import Migration from "wms-core/db/Migration";
import ModelFactory from "wms-core/db/ModelFactory";
import User from "wms-core/auth/models/User";
import UserNameComponent from "../models/UserNameComponent";
import {Connection} from "mysql";
export default class CreateUserPasswordsTable extends Migration {
public async install(connection: Connection): Promise<void> {
await this.query(`ALTER TABLE users
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`, connection);
}
public async rollback(connection: Connection): Promise<void> {
await this.query('ALTER TABLE users DROP COLUMN name', connection);
}
public registerModels(): void {
ModelFactory.get(User).addComponent(UserNameComponent);
}
}

View File

@ -0,0 +1,21 @@
import Migration from "wms-core/db/Migration";
import {Connection} from "mysql";
import ModelFactory from "wms-core/db/ModelFactory";
import User from "wms-core/auth/models/User";
import UserPasswordComponent from "../models/UserPasswordComponent";
export default class AddPasswordToUsers extends Migration {
public async install(connection: Connection): Promise<void> {
await this.query(`ALTER TABLE users
ADD COLUMN password VARCHAR(128) NOT NULL`, connection);
}
public async rollback(connection: Connection): Promise<void> {
await this.query(`ALTER TABLE users
DROP COLUMN password`, connection);
}
public registerModels(): void {
ModelFactory.get(User).addComponent(UserPasswordComponent);
}
}

View File

@ -1,18 +0,0 @@
import Migration from "wms-core/db/Migration";
import {query} from "wms-core/db/MysqlConnectionManager";
export default class CreateUserPasswordsTable extends Migration {
public async install(): Promise<void> {
await query('CREATE TABLE user_passwords(' +
'id INT NOT NULL AUTO_INCREMENT,' +
'user_id INT NOT NULL,' +
'password VARCHAR(256) NOT NULL,' +
'PRIMARY KEY(id),' +
'FOREIGN KEY user_pwd_fk (user_id) REFERENCES users (id) ON DELETE CASCADE' +
')');
}
public async rollback(): Promise<void> {
await query('DROP TABLE user_passwords');
}
}

View File

@ -1,18 +0,0 @@
import Migration from "wms-core/db/Migration";
import {query} from "wms-core/db/MysqlConnectionManager";
export default class CreateUserPasswordsTable extends Migration {
public async install(): Promise<void> {
await query('CREATE TABLE usernames(' +
'id INT NOT NULL AUTO_INCREMENT,' +
'user_id INT UNIQUE NOT NULL,' +
'username VARCHAR(64) UNIQUE NOT NULL,' +
'PRIMARY KEY(id),' +
'FOREIGN KEY user_name_fk (user_id) REFERENCES users (id) ON DELETE CASCADE' +
')');
}
public async rollback(): Promise<void> {
await query('DROP TABLE usernames');
}
}

View File

@ -0,0 +1,13 @@
import User from "wms-core/auth/models/User";
import ModelComponent from "wms-core/db/ModelComponent";
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
export default class UserNameComponent extends ModelComponent<User> {
public name!: string;
public init(): void {
this.setValidation('username').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model);
}
}

View File

@ -1,126 +0,0 @@
import Model from "wms-core/db/Model";
import Validator from "wms-core/db/Validator";
import User from "wms-core/auth/models/User";
import argon2 from "argon2";
import AuthProof from "wms-core/auth/AuthProof";
import {UserAlreadyExistsAuthError} from "wms-core/auth/AuthGuard";
import UserEmail from "wms-core/auth/models/UserEmail";
export default class UserPassword extends Model {
public static async getByEmail(email: string): Promise<UserPassword | null> {
const userEmail = await UserEmail.select('user_id')
.where('email', email)
.first();
return userEmail ? await UserPassword.select().where('user_id', userEmail.user_id).first() : null;
}
private user_id!: number;
private password!: string;
public constructor(props: any) {
super(props);
}
protected init(): void {
this.addProperty<number>('user_id', new Validator().defined().unique(this).exists(User, 'id'));
this.addProperty<string>('password', new Validator().defined());
}
public async setUser(userID: number): Promise<void> {
if (this.user_id) throw new Error(`Cannot change this password's user.`);
this.user_id = userID;
}
public async setPassword(rawPassword: string): Promise<void> {
this.password = await argon2.hash(rawPassword, {
timeCost: 10,
memoryCost: 4096,
parallelism: 4,
});
}
public async verifyPassword(passwordGuess: string): Promise<boolean> {
if (!this.password) return false;
return await argon2.verify(this.password, passwordGuess);
}
public isOwnedBy(userId: number): boolean {
return this.user_id === userId;
}
}
export class PasswordAuthProof implements AuthProof {
public static getProofForSession(session: Express.Session): PasswordAuthProof | null {
const authPasswordProof = session.auth_password_proof;
if (!authPasswordProof) return null;
return new PasswordAuthProof(authPasswordProof.email, authPasswordProof.authorized);
}
private readonly email?: string;
private authorized: boolean;
private userPassword?: UserPassword;
public constructor(email: string, authorized: boolean = false) {
this.email = email;
this.authorized = authorized;
}
public async getEmail(): Promise<string> {
return this.email!;
}
public async getUser(): Promise<User | null> {
const userEmail = await UserEmail.select()
.where('email', await this.getEmail())
.with('user')
.first();
return userEmail ? await userEmail.user.get() : null;
}
public async isAuthorized(): Promise<boolean> {
return this.authorized;
}
public async isOwnedBy(userId: number): Promise<boolean> {
const password = await this.getUserPassword();
return password !== null && password.isOwnedBy(userId);
}
public async isValid(): Promise<boolean> {
return await this.getUserPassword() !== null;
}
public async revoke(session: Express.Session): Promise<void> {
session.auth_password_proof = undefined;
}
private async getUserPassword(): Promise<UserPassword | null> {
return this.userPassword ? this.userPassword : await UserPassword.getByEmail(await this.getEmail());
}
public async authorize(passwordGuess: string, session?: Express.Session): Promise<boolean> {
const password = await this.getUserPassword();
if (!password || !await password.verifyPassword(passwordGuess)) return false;
this.authorized = true;
if (session) {
this.save(session);
}
return true;
}
public async register(password: string): Promise<UserPassword> {
if (await this.getUserPassword()) throw new UserAlreadyExistsAuthError(await this.getEmail());
this.userPassword = new UserPassword({});
await this.userPassword.setPassword(password);
return this.userPassword;
}
private save(session: Express.Session) {
session.auth_password_proof = {
email: this.email,
authorized: this.authorized,
};
}
}

View File

@ -0,0 +1,103 @@
import Validator from "wms-core/db/Validator";
import User from "wms-core/auth/models/User";
import argon2, {argon2id} from "argon2";
import AuthProof from "wms-core/auth/AuthProof";
import ModelComponent from "wms-core/db/ModelComponent";
export default class UserPasswordComponent extends ModelComponent<User> {
private password?: string = undefined;
public constructor(props: any) {
super(props);
}
public init(): void {
this.setValidation('password').acceptUndefined().maxLength(128);
}
public async setPassword(rawPassword: string): Promise<void> {
await new Validator<string>().defined().minLength(8).maxLength(512)
.execute('password', rawPassword, true);
this.password = await argon2.hash(rawPassword, {
timeCost: 10,
memoryCost: 65536,
parallelism: 4,
type: argon2id,
hashLength: 32,
});
}
public async verifyPassword(passwordGuess: string): Promise<boolean> {
if (!this.password) return false;
return await argon2.verify(this.password, passwordGuess);
}
}
export class PasswordAuthProof implements AuthProof<User> {
public static getProofForSession(session: Express.Session): PasswordAuthProof | null {
return session.auth_password_proof ? new PasswordAuthProof(session) : null;
}
public static createAuthorizedProofForRegistration(session: Express.Session): PasswordAuthProof {
const proofForSession = new PasswordAuthProof(session);
proofForSession.authorized = true;
proofForSession.save();
return proofForSession;
}
private readonly session: Express.Session;
private userID: number | null;
private authorized: boolean;
private userPassword: UserPasswordComponent | null = null;
public constructor(session: Express.Session) {
this.session = session;
this.authorized = session.auth_password_proof.authorized || false;
this.userID = session.auth_password_proof.userID || null;
}
public async getResource(): Promise<User | null> {
return await User.getById(this.userID);
}
public setResource(user: User) {
this.userID = user.id!;
this.save();
}
public async isAuthorized(): Promise<boolean> {
return this.authorized;
}
public async isValid(): Promise<boolean> {
return await this.getUserPassword() !== null;
}
public async revoke(): Promise<void> {
this.session.auth_password_proof = undefined;
}
private async getUserPassword(): Promise<UserPasswordComponent | null> {
if (!this.userPassword) {
this.userPassword = (await User.getById(this.userID))?.as(UserPasswordComponent) || null;
}
return this.userPassword;
}
public async authorize(passwordGuess: string): Promise<boolean> {
const password = await this.getUserPassword();
if (!password || !await password.verifyPassword(passwordGuess)) return false;
this.authorized = true;
this.save();
return true;
}
private save() {
this.session.auth_password_proof = {
authorized: this.authorized,
userID: this.userID,
};
}
}

View File

@ -1,38 +0,0 @@
import Model from "wms-core/db/Model";
import Validator from "wms-core/db/Validator";
import User from "wms-core/auth/models/User";
import {OneModelRelation} from "wms-core/db/ModelRelation";
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
export default class Username extends Model {
public static async fromUser(userID: number): Promise<Username | null> {
return await this.select().where('user_id', userID).first();
}
public static async getUserFromUsername(username: string): Promise<User | null> {
let model = await this.select()
.with('user')
.where('username', username.toLowerCase())
.first();
return model ? await model.user.get() : null;
}
private readonly user_id!: number;
public readonly user: OneModelRelation<this, User> = new OneModelRelation<this, User>(this, User, {
localKey: 'user_id',
foreignKey: 'id'
});
public username!: string;
constructor(data: any) {
super(data);
}
protected init(): void {
this.addProperty<number>('user_id', new Validator().defined().exists(User, 'id').unique(this));
this.addProperty<number>('username', new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this));
}
}

View File

@ -8067,9 +8067,9 @@ regenerator-runtime@^0.11.0:
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.toot.party/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
version "0.13.7"
resolved "https://registry.toot.party/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
regenerator-transform@^0.14.2:
version "0.14.5"
@ -9891,9 +9891,9 @@ widest-line@^3.1.0:
string-width "^4.0.0"
wms-core@^0:
version "0.17.2"
resolved "https://registry.toot.party/wms-core/-/wms-core-0.17.2.tgz#3a18695e23b1c8314d023d74865a38cfd6aaf3cb"
integrity sha512-oXPAviK7QpbllLuUb0qz/iIXBAXkuwQRkX5grNsw5pwpdi2SFgrtAHqagcW9TY1DuvmyobJ6qV9vbW4kSbveiw==
version "0.19.5"
resolved "https://registry.toot.party/wms-core/-/wms-core-0.19.5.tgz#e366f449a6c9c5bdbd2fe8aa8e653cdffe821af7"
integrity sha512-ftXCHxbZlBQpLjXO1xpabX5+lcYYgw3stX/vPQuR1r0XLmFG5kn3U3QhlYAWx51dyOqqWDidP8bJ5EHBT9SByQ==
dependencies:
argon2 "^0.26.2"
compression "^1.7.4"