diff --git a/src/App.ts b/src/App.ts index c3dfbae..865dd3e 100644 --- a/src/App.ts +++ b/src/App.ts @@ -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 { - public async getProofForSession(session: Express.Session): Promise { - return PasswordAuthProof.getProofForSession(session); + this.use(new AuthComponent(new class extends AuthGuard { + public async getProofForSession(session: Express.Session): Promise { + 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()); diff --git a/src/LDAPServerComponent.ts b/src/LDAPServerComponent.ts index db042da..968fb1f 100644 --- a/src/LDAPServerComponent.ts +++ b/src/LDAPServerComponent.ts @@ -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 { diff --git a/src/migrations/AddNameToUsers.ts b/src/migrations/AddNameToUsers.ts new file mode 100644 index 0000000..5834240 --- /dev/null +++ b/src/migrations/AddNameToUsers.ts @@ -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 { + await this.query(`ALTER TABLE users + ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`, connection); + } + + public async rollback(connection: Connection): Promise { + await this.query('ALTER TABLE users DROP COLUMN name', connection); + } + + public registerModels(): void { + ModelFactory.get(User).addComponent(UserNameComponent); + } +} diff --git a/src/migrations/AddPasswordToUsers.ts b/src/migrations/AddPasswordToUsers.ts new file mode 100644 index 0000000..fb44b62 --- /dev/null +++ b/src/migrations/AddPasswordToUsers.ts @@ -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 { + await this.query(`ALTER TABLE users + ADD COLUMN password VARCHAR(128) NOT NULL`, connection); + } + + public async rollback(connection: Connection): Promise { + await this.query(`ALTER TABLE users + DROP COLUMN password`, connection); + } + + public registerModels(): void { + ModelFactory.get(User).addComponent(UserPasswordComponent); + } +} \ No newline at end of file diff --git a/src/migrations/CreateUserPasswordsTable.ts b/src/migrations/CreateUserPasswordsTable.ts deleted file mode 100644 index 40509eb..0000000 --- a/src/migrations/CreateUserPasswordsTable.ts +++ /dev/null @@ -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 { - 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 { - await query('DROP TABLE user_passwords'); - } -} \ No newline at end of file diff --git a/src/migrations/CreateUsernamesTable.ts b/src/migrations/CreateUsernamesTable.ts deleted file mode 100644 index 1ee20db..0000000 --- a/src/migrations/CreateUsernamesTable.ts +++ /dev/null @@ -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 { - 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 { - await query('DROP TABLE usernames'); - } -} \ No newline at end of file diff --git a/src/models/UserNameComponent.ts b/src/models/UserNameComponent.ts new file mode 100644 index 0000000..d444327 --- /dev/null +++ b/src/models/UserNameComponent.ts @@ -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 { + public name!: string; + + public init(): void { + this.setValidation('username').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model); + } + +} \ No newline at end of file diff --git a/src/models/UserPassword.ts b/src/models/UserPassword.ts deleted file mode 100644 index d03b58e..0000000 --- a/src/models/UserPassword.ts +++ /dev/null @@ -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 { - 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('user_id', new Validator().defined().unique(this).exists(User, 'id')); - this.addProperty('password', new Validator().defined()); - } - - public async setUser(userID: number): Promise { - if (this.user_id) throw new Error(`Cannot change this password's user.`); - this.user_id = userID; - } - - public async setPassword(rawPassword: string): Promise { - this.password = await argon2.hash(rawPassword, { - timeCost: 10, - memoryCost: 4096, - parallelism: 4, - }); - } - - public async verifyPassword(passwordGuess: string): Promise { - 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 { - return this.email!; - } - - public async getUser(): Promise { - const userEmail = await UserEmail.select() - .where('email', await this.getEmail()) - .with('user') - .first(); - return userEmail ? await userEmail.user.get() : null; - } - - public async isAuthorized(): Promise { - return this.authorized; - } - - public async isOwnedBy(userId: number): Promise { - const password = await this.getUserPassword(); - return password !== null && password.isOwnedBy(userId); - } - - public async isValid(): Promise { - return await this.getUserPassword() !== null; - } - - public async revoke(session: Express.Session): Promise { - session.auth_password_proof = undefined; - } - - private async getUserPassword(): Promise { - return this.userPassword ? this.userPassword : await UserPassword.getByEmail(await this.getEmail()); - } - - public async authorize(passwordGuess: string, session?: Express.Session): Promise { - 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 { - 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, - }; - } -} \ No newline at end of file diff --git a/src/models/UserPasswordComponent.ts b/src/models/UserPasswordComponent.ts new file mode 100644 index 0000000..0794090 --- /dev/null +++ b/src/models/UserPasswordComponent.ts @@ -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 { + 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 { + await new Validator().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 { + if (!this.password) return false; + + return await argon2.verify(this.password, passwordGuess); + } +} + +export class PasswordAuthProof implements AuthProof { + 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 { + return await User.getById(this.userID); + } + + public setResource(user: User) { + this.userID = user.id!; + this.save(); + } + + public async isAuthorized(): Promise { + return this.authorized; + } + + public async isValid(): Promise { + return await this.getUserPassword() !== null; + } + + public async revoke(): Promise { + this.session.auth_password_proof = undefined; + } + + private async getUserPassword(): Promise { + if (!this.userPassword) { + this.userPassword = (await User.getById(this.userID))?.as(UserPasswordComponent) || null; + } + return this.userPassword; + } + + public async authorize(passwordGuess: string): Promise { + 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, + }; + } +} \ No newline at end of file diff --git a/src/models/Username.ts b/src/models/Username.ts deleted file mode 100644 index 2e20e8e..0000000 --- a/src/models/Username.ts +++ /dev/null @@ -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 { - return await this.select().where('user_id', userID).first(); - } - - public static async getUserFromUsername(username: string): Promise { - 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 = new OneModelRelation(this, User, { - localKey: 'user_id', - foreignKey: 'id' - }); - public username!: string; - - - constructor(data: any) { - super(data); - } - - protected init(): void { - this.addProperty('user_id', new Validator().defined().exists(User, 'id').unique(this)); - this.addProperty('username', new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this)); - } - -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e3542e9..a19cb61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"