UNPKG

@accounts/two-factor

Version:

[![npm](https://img.shields.io/npm/v/@accounts/two-factor.svg)](https://www.npmjs.com/package/@accounts/two-factor) ![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)

127 lines (112 loc) 3.57 kB
import { type GeneratedSecret, generateSecret, totp } from '@levminer/speakeasy'; import { type User, type DatabaseInterfaceUser } from '@accounts/types'; import { errors } from './errors'; import { type AccountsTwoFactorOptions } from './types'; import { getUserTwoFactorService } from './utils'; const defaultOptions = { secretLength: 20, window: 0, errors, }; export class TwoFactor<CustomUser extends User = User> { private options: AccountsTwoFactorOptions & typeof defaultOptions; private db!: DatabaseInterfaceUser<CustomUser>; private serviceName = 'two-factor'; private verifyTOTPCode(secret: string, code: string): Boolean { try { const verified = totp.verify({ secret, encoding: 'base32', token: code, window: this.options.window, }); if (verified) return true; } catch (e) { // } return false; } constructor(options: AccountsTwoFactorOptions = {}) { this.options = { ...defaultOptions, ...options }; } /** * Set two factor store */ public setUserStore(store: DatabaseInterfaceUser<CustomUser>): void { this.db = store; } /** * Authenticate a user with a 2fa code */ public async authenticate(user: User, code: string): Promise<void> { if (!code) { throw new Error(this.options.errors.codeRequired); } const twoFactorService = getUserTwoFactorService(user); // If user does not have 2fa set return error if (!twoFactorService) { throw new Error(this.options.errors.userTwoFactorNotSet); } if (!this.verifyTOTPCode(twoFactorService.secret.base32, code)) { throw new Error(this.options.errors.codeDidNotMatch); } } /** * Generate a new two factor secret */ public getNewAuthSecret(): GeneratedSecret { return generateSecret({ length: this.options.secretLength, name: this.options.appName, }); } /** * Verify the code is correct * Add the code to the user profile * Throw if user already have 2fa enabled */ public async set(userId: string, secret: GeneratedSecret, code: string): Promise<void> { if (!code) { throw new Error(this.options.errors.codeRequired); } const user = await this.db.findUserById(userId); if (!user) { throw new Error(this.options.errors.userNotFound); } let twoFactorService = getUserTwoFactorService(user); // If user already have 2fa return error if (twoFactorService) { throw new Error(this.options.errors.userTwoFactorAlreadySet); } if (this.verifyTOTPCode(secret.base32, code)) { twoFactorService = { secret, }; await this.db.setService(userId, this.serviceName, twoFactorService); } else { throw new Error(this.options.errors.codeDidNotMatch); } } /** * Remove two factor for a user */ public async unset(userId: string, code: string): Promise<void> { if (!code) { throw new Error(this.options.errors.codeRequired); } const user = await this.db.findUserById(userId); if (!user) { throw new Error(this.options.errors.userNotFound); } const twoFactorService = getUserTwoFactorService(user); // If user does not have 2fa set return error if (!twoFactorService) { throw new Error(this.options.errors.userTwoFactorNotSet); } if (this.verifyTOTPCode(twoFactorService.secret.base32, code)) { await this.db.unsetService(userId, this.serviceName); } else { throw new Error(this.options.errors.codeDidNotMatch); } } }