@accounts/two-factor
Version:
[](https://www.npmjs.com/package/@accounts/two-factor) 
127 lines (112 loc) • 3.57 kB
text/typescript
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);
}
}
}