-
-
Notifications
You must be signed in to change notification settings - Fork 142
/
two-factor.ts
126 lines (112 loc) · 3.49 KB
/
two-factor.ts
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
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);
}
}
}