All files / src two-factor.ts

100% Statements 50/50
95.45% Branches 21/22
100% Functions 7/7
100% Lines 46/46
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 1322x   2x   2x   2x           2x     1x   1x 1x           2x 1x           2x 4x 1x     3x   3x 1x   2x               1x             2x 5x                     2x 5x 1x     4x 4x 1x   3x   3x 1x       1x             1x     1x   1x             2x 5x 1x     4x 4x 1x   3x   3x 1x   2x               1x   1x     2x  
import * as speakeasy from 'speakeasy';
import { User, DatabaseInterface } from '@accounts/types';
import { errors } from './errors';
import { AccountsTwoFactorOptions } from './types';
import { getUserTwoFactorService } from './utils';
 
const defaultOptions = {
  secretLength: 20,
  window: 0,
  errors,
};
 
export class TwoFactor {
  private options: AccountsTwoFactorOptions & typeof defaultOptions;
  private db!: DatabaseInterface;
  private serviceName = 'two-factor';
 
  constructor(Ioptions: AccountsTwoFactorOptions = {}) {
    this.options = { ...defaultOptions, ...options };
  }
 
  /**
   * Set two factor store
   */
  public setStore(store: DatabaseInterface): 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 (
      !speakeasy.totp.verify({
        secret: twoFactorService.secret.base32,
        encoding: 'base32',
        token: code,
        window: this.options.window,
      })
    ) {
      throw new Error(this.options.errors.codeDidNotMatch);
    }
  }
 
  /**
   * Generate a new two factor secret
   */
  public getNewAuthSecret(): speakeasy.Key {
    return speakeasy.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: speakeasy.Key, 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 (
      speakeasy.totp.verify({
        secret: secret.base32,
        encoding: 'base32',
        token: code,
        window: this.options.window,
      })
    ) {
      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 (
      speakeasy.totp.verify({
        secret: twoFactorService.secret.base32,
        encoding: 'base32',
        token: code,
        window: this.options.window,
      })
    ) {
      this.db.unsetService(userId, this.serviceName);
    } else {
      throw new Error(this.options.errors.codeDidNotMatch);
    }
  }
}