@c9up/warden 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/Guard.d.ts +9 -0
- package/dist/Guard.d.ts.map +1 -1
- package/dist/Guard.js +16 -0
- package/dist/Guard.js.map +1 -1
- package/dist/RedisBlacklistDriver.d.ts +32 -0
- package/dist/RedisBlacklistDriver.d.ts.map +1 -0
- package/dist/RedisBlacklistDriver.js +37 -0
- package/dist/RedisBlacklistDriver.js.map +1 -0
- package/dist/ResilientBlacklistDriver.d.ts +35 -0
- package/dist/ResilientBlacklistDriver.d.ts.map +1 -0
- package/dist/ResilientBlacklistDriver.js +61 -0
- package/dist/ResilientBlacklistDriver.js.map +1 -0
- package/dist/WardenProvider.d.ts.map +1 -1
- package/dist/WardenProvider.js +10 -0
- package/dist/WardenProvider.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/mfa/BackupCodesProvider.d.ts +39 -0
- package/dist/mfa/BackupCodesProvider.d.ts.map +1 -0
- package/dist/mfa/BackupCodesProvider.js +87 -0
- package/dist/mfa/BackupCodesProvider.js.map +1 -0
- package/dist/mfa/MfaManager.d.ts +107 -0
- package/dist/mfa/MfaManager.d.ts.map +1 -0
- package/dist/mfa/MfaManager.js +219 -0
- package/dist/mfa/MfaManager.js.map +1 -0
- package/dist/mfa/OtpProvider.d.ts +67 -0
- package/dist/mfa/OtpProvider.d.ts.map +1 -0
- package/dist/mfa/OtpProvider.js +104 -0
- package/dist/mfa/OtpProvider.js.map +1 -0
- package/dist/mfa/TotpProvider.d.ts +52 -0
- package/dist/mfa/TotpProvider.d.ts.map +1 -0
- package/dist/mfa/TotpProvider.js +103 -0
- package/dist/mfa/TotpProvider.js.map +1 -0
- package/dist/mfa/WebauthnProvider.d.ts +175 -0
- package/dist/mfa/WebauthnProvider.d.ts.map +1 -0
- package/dist/mfa/WebauthnProvider.js +239 -0
- package/dist/mfa/WebauthnProvider.js.map +1 -0
- package/dist/mfa/base32.d.ts +13 -0
- package/dist/mfa/base32.d.ts.map +1 -0
- package/dist/mfa/base32.js +52 -0
- package/dist/mfa/base32.js.map +1 -0
- package/dist/mfa/webauthn-codec.d.ts +58 -0
- package/dist/mfa/webauthn-codec.d.ts.map +1 -0
- package/dist/mfa/webauthn-codec.js +221 -0
- package/dist/mfa/webauthn-codec.js.map +1 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +13 -1
- package/dist/middleware.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/Guard.ts +21 -0
- package/src/RedisBlacklistDriver.ts +60 -0
- package/src/ResilientBlacklistDriver.ts +84 -0
- package/src/WardenProvider.ts +11 -0
- package/src/config.ts +7 -0
- package/src/index.ts +58 -0
- package/src/mfa/BackupCodesProvider.ts +125 -0
- package/src/mfa/MfaManager.ts +307 -0
- package/src/mfa/OtpProvider.ts +177 -0
- package/src/mfa/TotpProvider.ts +140 -0
- package/src/mfa/WebauthnProvider.ts +416 -0
- package/src/mfa/base32.ts +54 -0
- package/src/mfa/webauthn-codec.ts +264 -0
- package/src/middleware.ts +14 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MfaManager — orchestrates the stateful "enable 2FA" lifecycle for a user:
|
|
3
|
+
* TOTP enrollment (pending → confirmed) and single-use backup codes, over a
|
|
4
|
+
* pluggable factor store (in-memory default).
|
|
5
|
+
*
|
|
6
|
+
* OTP (email/SMS) and WebAuthn are NOT folded in here: OTP is ephemeral
|
|
7
|
+
* (challenge/response, no enrollment) and WebAuthn already persists its own
|
|
8
|
+
* passkeys in its credential store. Use `OtpProvider` / `WebauthnProvider`
|
|
9
|
+
* directly for those, alongside this manager.
|
|
10
|
+
*/
|
|
11
|
+
import type { BackupCodesProvider } from "./BackupCodesProvider.js";
|
|
12
|
+
import type { TotpProvider } from "./TotpProvider.js";
|
|
13
|
+
export type MfaFactorKind = "totp" | "backup_codes";
|
|
14
|
+
export interface MfaFactor {
|
|
15
|
+
id: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
kind: MfaFactorKind;
|
|
18
|
+
label?: string;
|
|
19
|
+
/** TOTP shared secret (base32), present for `totp` factors. */
|
|
20
|
+
secret?: string;
|
|
21
|
+
/** Salted hashes of the remaining backup codes, for `backup_codes` factors. */
|
|
22
|
+
backupHashes?: string[];
|
|
23
|
+
/** Epoch ms when the factor became usable; absent while pending. */
|
|
24
|
+
confirmedAt?: number;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
/** Public, secret-free view of an enrolled factor. */
|
|
28
|
+
export interface MfaFactorSummary {
|
|
29
|
+
id: string;
|
|
30
|
+
kind: MfaFactorKind;
|
|
31
|
+
label?: string;
|
|
32
|
+
confirmed: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface MfaFactorStore {
|
|
35
|
+
save(factor: MfaFactor): Promise<void>;
|
|
36
|
+
findById(id: string): Promise<MfaFactor | null>;
|
|
37
|
+
findByUser(userId: string): Promise<MfaFactor[]>;
|
|
38
|
+
delete(id: string): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare class MemoryMfaFactorStore implements MfaFactorStore {
|
|
41
|
+
#private;
|
|
42
|
+
save(factor: MfaFactor): Promise<void>;
|
|
43
|
+
findById(id: string): Promise<MfaFactor | null>;
|
|
44
|
+
findByUser(userId: string): Promise<MfaFactor[]>;
|
|
45
|
+
delete(id: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface MfaRateLimitConfig {
|
|
48
|
+
/** Failed verifications allowed per user before lockout. Default `5`. */
|
|
49
|
+
maxAttempts?: number;
|
|
50
|
+
/** Lockout / counter window in seconds. Default `900` (15 min). */
|
|
51
|
+
windowSeconds?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface MfaManagerConfig {
|
|
54
|
+
/** App/brand name shown in the authenticator (the TOTP issuer). */
|
|
55
|
+
issuer: string;
|
|
56
|
+
totp?: TotpProvider;
|
|
57
|
+
backupCodes?: BackupCodesProvider;
|
|
58
|
+
store?: MfaFactorStore;
|
|
59
|
+
/** Per-user brute-force protection on `verify()`. Defaults to 5 / 15 min. */
|
|
60
|
+
rateLimit?: MfaRateLimitConfig;
|
|
61
|
+
}
|
|
62
|
+
export declare class MfaManager {
|
|
63
|
+
#private;
|
|
64
|
+
constructor(config: MfaManagerConfig);
|
|
65
|
+
/**
|
|
66
|
+
* Begin TOTP enrollment: persist a pending factor and return the secret +
|
|
67
|
+
* `otpauth://` URI to show as a QR code. The factor is unusable until
|
|
68
|
+
* `confirmTotp()` succeeds.
|
|
69
|
+
*/
|
|
70
|
+
enrollTotp(user: {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
}, label?: string): Promise<{
|
|
74
|
+
factorId: string;
|
|
75
|
+
secret: string;
|
|
76
|
+
uri: string;
|
|
77
|
+
}>;
|
|
78
|
+
/** Confirm a pending TOTP factor by verifying a first code. */
|
|
79
|
+
confirmTotp(factorId: string, code: string): Promise<boolean>;
|
|
80
|
+
/** Verify a TOTP code against the user's confirmed authenticator factors. */
|
|
81
|
+
verifyTotp(userId: string, code: string): Promise<boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Generate (or regenerate) the user's backup codes. Returns the plaintext
|
|
84
|
+
* codes to display ONCE; only hashes are stored. Any prior backup-code
|
|
85
|
+
* factor is replaced.
|
|
86
|
+
*/
|
|
87
|
+
createBackupCodes(userId: string, label?: string): Promise<string[]>;
|
|
88
|
+
/** Verify and consume one backup code for the user. */
|
|
89
|
+
verifyBackupCode(userId: string, code: string): Promise<boolean>;
|
|
90
|
+
/**
|
|
91
|
+
* Verify a code entered in a single "2FA code" field: tries the user's TOTP
|
|
92
|
+
* factors first, then falls back to consuming a backup code.
|
|
93
|
+
*/
|
|
94
|
+
verify(userId: string, code: string): Promise<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* Whether the user is currently locked out of `verify()` after too many
|
|
97
|
+
* failed attempts. Use it to surface a "try again later" message.
|
|
98
|
+
*/
|
|
99
|
+
isLocked(userId: string): boolean;
|
|
100
|
+
/** List the user's factors without exposing any secret material. */
|
|
101
|
+
listFactors(userId: string): Promise<MfaFactorSummary[]>;
|
|
102
|
+
/** Whether the user has at least one confirmed factor. */
|
|
103
|
+
isEnabled(userId: string): Promise<boolean>;
|
|
104
|
+
/** Remove a factor (e.g. the user disables their authenticator). */
|
|
105
|
+
disableFactor(factorId: string): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=MfaManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MfaManager.d.ts","sourceRoot":"","sources":["../../src/mfa/MfaManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MfaManager — orchestrates the stateful "enable 2FA" lifecycle for a user:
|
|
3
|
+
* TOTP enrollment (pending → confirmed) and single-use backup codes, over a
|
|
4
|
+
* pluggable factor store (in-memory default).
|
|
5
|
+
*
|
|
6
|
+
* OTP (email/SMS) and WebAuthn are NOT folded in here: OTP is ephemeral
|
|
7
|
+
* (challenge/response, no enrollment) and WebAuthn already persists its own
|
|
8
|
+
* passkeys in its credential store. Use `OtpProvider` / `WebauthnProvider`
|
|
9
|
+
* directly for those, alongside this manager.
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { WardenError } from "../errors.js";
|
|
13
|
+
export class MemoryMfaFactorStore {
|
|
14
|
+
#store = new Map();
|
|
15
|
+
async save(factor) {
|
|
16
|
+
this.#store.set(factor.id, factor);
|
|
17
|
+
}
|
|
18
|
+
async findById(id) {
|
|
19
|
+
return this.#store.get(id) ?? null;
|
|
20
|
+
}
|
|
21
|
+
async findByUser(userId) {
|
|
22
|
+
return [...this.#store.values()].filter((f) => f.userId === userId);
|
|
23
|
+
}
|
|
24
|
+
async delete(id) {
|
|
25
|
+
this.#store.delete(id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class MfaManager {
|
|
29
|
+
#issuer;
|
|
30
|
+
#totp;
|
|
31
|
+
#backupCodes;
|
|
32
|
+
#store;
|
|
33
|
+
#maxAttempts;
|
|
34
|
+
#windowMs;
|
|
35
|
+
#attempts = new Map();
|
|
36
|
+
constructor(config) {
|
|
37
|
+
if (!config?.issuer) {
|
|
38
|
+
throw new WardenError("INVALID_CONFIG", "MfaManager requires an issuer");
|
|
39
|
+
}
|
|
40
|
+
this.#issuer = config.issuer;
|
|
41
|
+
this.#totp = config.totp;
|
|
42
|
+
this.#backupCodes = config.backupCodes;
|
|
43
|
+
this.#store = config.store ?? new MemoryMfaFactorStore();
|
|
44
|
+
this.#maxAttempts = config.rateLimit?.maxAttempts ?? 5;
|
|
45
|
+
this.#windowMs = (config.rateLimit?.windowSeconds ?? 900) * 1000;
|
|
46
|
+
}
|
|
47
|
+
// ── TOTP enrollment ──────────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Begin TOTP enrollment: persist a pending factor and return the secret +
|
|
50
|
+
* `otpauth://` URI to show as a QR code. The factor is unusable until
|
|
51
|
+
* `confirmTotp()` succeeds.
|
|
52
|
+
*/
|
|
53
|
+
async enrollTotp(user, label) {
|
|
54
|
+
const totp = this.#requireTotp();
|
|
55
|
+
const { secret, uri } = totp.enroll(user.name, this.#issuer);
|
|
56
|
+
const factor = {
|
|
57
|
+
id: newId(),
|
|
58
|
+
userId: user.id,
|
|
59
|
+
kind: "totp",
|
|
60
|
+
label,
|
|
61
|
+
secret,
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
await this.#store.save(factor);
|
|
65
|
+
return { factorId: factor.id, secret, uri };
|
|
66
|
+
}
|
|
67
|
+
/** Confirm a pending TOTP factor by verifying a first code. */
|
|
68
|
+
async confirmTotp(factorId, code) {
|
|
69
|
+
const totp = this.#requireTotp();
|
|
70
|
+
const factor = await this.#store.findById(factorId);
|
|
71
|
+
if (!factor || factor.kind !== "totp" || !factor.secret) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!totp.verify(factor.secret, code)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
await this.#store.save({ ...factor, confirmedAt: Date.now() });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** Verify a TOTP code against the user's confirmed authenticator factors. */
|
|
81
|
+
async verifyTotp(userId, code) {
|
|
82
|
+
const totp = this.#requireTotp();
|
|
83
|
+
const factors = await this.#store.findByUser(userId);
|
|
84
|
+
return factors.some((f) => f.kind === "totp" &&
|
|
85
|
+
f.confirmedAt !== undefined &&
|
|
86
|
+
f.secret !== undefined &&
|
|
87
|
+
totp.verify(f.secret, code));
|
|
88
|
+
}
|
|
89
|
+
// ── Backup codes ─────────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Generate (or regenerate) the user's backup codes. Returns the plaintext
|
|
92
|
+
* codes to display ONCE; only hashes are stored. Any prior backup-code
|
|
93
|
+
* factor is replaced.
|
|
94
|
+
*/
|
|
95
|
+
async createBackupCodes(userId, label) {
|
|
96
|
+
const provider = this.#requireBackupCodes();
|
|
97
|
+
const existing = await this.#store.findByUser(userId);
|
|
98
|
+
for (const f of existing) {
|
|
99
|
+
if (f.kind === "backup_codes") {
|
|
100
|
+
await this.#store.delete(f.id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const { codes, hashes } = provider.generate();
|
|
104
|
+
await this.#store.save({
|
|
105
|
+
id: newId(),
|
|
106
|
+
userId,
|
|
107
|
+
kind: "backup_codes",
|
|
108
|
+
label,
|
|
109
|
+
backupHashes: hashes,
|
|
110
|
+
// Backup codes are usable as soon as they are shown.
|
|
111
|
+
confirmedAt: Date.now(),
|
|
112
|
+
createdAt: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
return codes;
|
|
115
|
+
}
|
|
116
|
+
/** Verify and consume one backup code for the user. */
|
|
117
|
+
async verifyBackupCode(userId, code) {
|
|
118
|
+
const provider = this.#requireBackupCodes();
|
|
119
|
+
const factors = await this.#store.findByUser(userId);
|
|
120
|
+
for (const factor of factors) {
|
|
121
|
+
if (factor.kind !== "backup_codes" || !factor.backupHashes) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const result = provider.verify(factor.backupHashes, code);
|
|
125
|
+
if (result.ok) {
|
|
126
|
+
await this.#store.save({ ...factor, backupHashes: result.remaining });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
// ── Unified verify + status ──────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Verify a code entered in a single "2FA code" field: tries the user's TOTP
|
|
135
|
+
* factors first, then falls back to consuming a backup code.
|
|
136
|
+
*/
|
|
137
|
+
async verify(userId, code) {
|
|
138
|
+
if (this.#isLocked(userId)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
let ok = false;
|
|
142
|
+
if (this.#totp && (await this.verifyTotp(userId, code))) {
|
|
143
|
+
ok = true;
|
|
144
|
+
}
|
|
145
|
+
else if (this.#backupCodes &&
|
|
146
|
+
(await this.verifyBackupCode(userId, code))) {
|
|
147
|
+
ok = true;
|
|
148
|
+
}
|
|
149
|
+
if (ok) {
|
|
150
|
+
this.#attempts.delete(userId);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.#recordFailure(userId);
|
|
154
|
+
}
|
|
155
|
+
return ok;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Whether the user is currently locked out of `verify()` after too many
|
|
159
|
+
* failed attempts. Use it to surface a "try again later" message.
|
|
160
|
+
*/
|
|
161
|
+
isLocked(userId) {
|
|
162
|
+
return this.#isLocked(userId);
|
|
163
|
+
}
|
|
164
|
+
/** List the user's factors without exposing any secret material. */
|
|
165
|
+
async listFactors(userId) {
|
|
166
|
+
const factors = await this.#store.findByUser(userId);
|
|
167
|
+
return factors.map((f) => ({
|
|
168
|
+
id: f.id,
|
|
169
|
+
kind: f.kind,
|
|
170
|
+
label: f.label,
|
|
171
|
+
confirmed: f.confirmedAt !== undefined,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
/** Whether the user has at least one confirmed factor. */
|
|
175
|
+
async isEnabled(userId) {
|
|
176
|
+
const factors = await this.#store.findByUser(userId);
|
|
177
|
+
return factors.some((f) => f.confirmedAt !== undefined);
|
|
178
|
+
}
|
|
179
|
+
/** Remove a factor (e.g. the user disables their authenticator). */
|
|
180
|
+
async disableFactor(factorId) {
|
|
181
|
+
await this.#store.delete(factorId);
|
|
182
|
+
}
|
|
183
|
+
#isLocked(userId) {
|
|
184
|
+
const entry = this.#attempts.get(userId);
|
|
185
|
+
if (!entry) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (entry.resetAt < Date.now()) {
|
|
189
|
+
this.#attempts.delete(userId);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return entry.count >= this.#maxAttempts;
|
|
193
|
+
}
|
|
194
|
+
#recordFailure(userId) {
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
let entry = this.#attempts.get(userId);
|
|
197
|
+
if (!entry || entry.resetAt < now) {
|
|
198
|
+
entry = { count: 0, resetAt: now + this.#windowMs };
|
|
199
|
+
this.#attempts.set(userId, entry);
|
|
200
|
+
}
|
|
201
|
+
entry.count++;
|
|
202
|
+
}
|
|
203
|
+
#requireTotp() {
|
|
204
|
+
if (!this.#totp) {
|
|
205
|
+
throw new WardenError("INVALID_CONFIG", "MfaManager has no TotpProvider configured");
|
|
206
|
+
}
|
|
207
|
+
return this.#totp;
|
|
208
|
+
}
|
|
209
|
+
#requireBackupCodes() {
|
|
210
|
+
if (!this.#backupCodes) {
|
|
211
|
+
throw new WardenError("INVALID_CONFIG", "MfaManager has no BackupCodesProvider configured");
|
|
212
|
+
}
|
|
213
|
+
return this.#backupCodes;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function newId() {
|
|
217
|
+
return randomBytes(16).toString("hex");
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=MfaManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MfaManager.js","sourceRoot":"","sources":["../../src/mfa/MfaManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAmC3C,MAAM,OAAO,oBAAoB;IAChC,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IACtC,KAAK,CAAC,IAAI,CAAC,MAAiB;QAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,EAAU;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC;IACpC,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,MAAc;QAC9B,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IACrE,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,EAAU;QACtB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC;CACD;AAwBD,MAAM,OAAO,UAAU;IACb,OAAO,CAAS;IAChB,KAAK,CAAgB;IACrB,YAAY,CAAuB;IACnC,MAAM,CAAiB;IACvB,YAAY,CAAS;IACrB,SAAS,CAAS;IAClB,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;IAErD,YAAY,MAAwB;QACnC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YACrB,MAAM,IAAI,WAAW,CAAC,gBAAgB,EAAE,+BAA+B,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,WAAW,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,IAAI,oBAAoB,EAAE,CAAC;QACzD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,SAAS,EAAE,WAAW,IAAI,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC;IAClE,CAAC;IAED,oEAAoE;IAEpE;;;;OAIG;IACH,KAAK,CAAC,UAAU,CACf,IAAkC,EAClC,KAAc;QAEd,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAc;YACzB,EAAE,EAAE,KAAK,EAAE;YACX,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,IAAI,EAAE,MAAM;YACZ,KAAK;YACL,MAAM;YACN,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACrB,CAAC;QACF,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IAC7C,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,WAAW,CAAC,QAAgB,EAAE,IAAY;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACzD,OAAO,KAAK,CAAC;QACd,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;YACvC,OAAO,KAAK,CAAC;QACd,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC;IACb,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,UAAU,CAAC,MAAc,EAAE,IAAY;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACrD,OAAO,OAAO,CAAC,IAAI,CAClB,CAAC,CAAC,EAAE,EAAE,CACL,CAAC,CAAC,IAAI,KAAK,MAAM;YACjB,CAAC,CAAC,WAAW,KAAK,SAAS;YAC3B,CAAC,CAAC,MAAM,KAAK,SAAS;YACtB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAC5B,CAAC;IACH,CAAC;IAED,oEAAoE;IAEpE;;;;OAIG;IACH,KAAK,CAAC,iBAAiB,CAAC,MAAc,EAAE,KAAc;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACtD,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YAC1B,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;gBAC/B,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAChC,CAAC;QACF,CAAC;QACD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC9C,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACtB,EAAE,EAAE,KAAK,EAAE;YACX,MAAM;YACN,IAAI,EAAE,cAAc;YACpB,KAAK;YACL,YAAY,EAAE,MAAM;YACpB,qDAAqD;YACrD,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;YACvB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACrB,CAAC,CAAC;QACH,OAAO,KAAK,CAAC;IACd,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,gBAAgB,CAAC,MAAc,EAAE,IAAY;QAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACrD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC9B,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;gBAC5D,SAAS;YACV,CAAC;YACD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;YAC1D,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;gBACf,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;gBACtE,OAAO,IAAI,CAAC;YACb,CAAC;QACF,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;IAED,oEAAoE;IAEpE;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,IAAY;QACxC,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC;QACd,CAAC;QACD,IAAI,EAAE,GAAG,KAAK,CAAC;QACf,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;YACzD,EAAE,GAAG,IAAI,CAAC;QACX,CAAC;aAAM,IACN,IAAI,CAAC,YAAY;YACjB,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAC1C,CAAC;YACF,EAAE,GAAG,IAAI,CAAC;QACX,CAAC;QACD,IAAI,EAAE,EAAE,CAAC;YACR,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC;QACD,OAAO,EAAE,CAAC;IACX,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,MAAc;QACtB,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,oEAAoE;IACpE,KAAK,CAAC,WAAW,CAAC,MAAc;QAC/B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACrD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1B,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,SAAS,EAAE,CAAC,CAAC,WAAW,KAAK,SAAS;SACtC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,SAAS,CAAC,MAAc;QAC7B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QACrD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC;IACzD,CAAC;IAED,oEAAoE;IACpE,KAAK,CAAC,aAAa,CAAC,QAAgB;QACnC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IAED,SAAS,CAAC,MAAc;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,EAAE,CAAC;YACZ,OAAO,KAAK,CAAC;QACd,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,OAAO,KAAK,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC;IACzC,CAAC;IAED,cAAc,CAAC,MAAc;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC;YACnC,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YACpD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;QACD,KAAK,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED,YAAY;QACX,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,IAAI,WAAW,CACpB,gBAAgB,EAChB,2CAA2C,CAC3C,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC;IACnB,CAAC;IAED,mBAAmB;QAClB,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACxB,MAAM,IAAI,WAAW,CACpB,gBAAgB,EAChB,kDAAkD,CAClD,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC;IAC1B,CAAC;CACD;AAED,SAAS,KAAK;IACb,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtpProvider — delivered one-time passcodes (email / SMS). Unlike TOTP this is
|
|
3
|
+
* a challenge/response flow: `start()` mints a code, persists it (hashed, with
|
|
4
|
+
* an expiry and an attempt budget) and hands it to a delivery channel; the user
|
|
5
|
+
* later submits it to `verify()`.
|
|
6
|
+
*
|
|
7
|
+
* Delivery is pluggable — the app supplies an email or SMS channel. The store
|
|
8
|
+
* is pluggable too, with an in-memory default mirroring the rest of warden.
|
|
9
|
+
*/
|
|
10
|
+
/** Sends the code to the user. Implemented by the consuming app (email/SMS). */
|
|
11
|
+
export interface OtpDeliveryChannel {
|
|
12
|
+
send(recipient: string, code: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** A persisted, pending OTP challenge. */
|
|
15
|
+
export interface OtpChallenge {
|
|
16
|
+
id: string;
|
|
17
|
+
recipient: string;
|
|
18
|
+
/** Salted hash of the code — never the plaintext. */
|
|
19
|
+
hash: string;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
attempts: number;
|
|
22
|
+
}
|
|
23
|
+
/** Storage for pending challenges. */
|
|
24
|
+
export interface OtpChallengeStore {
|
|
25
|
+
save(challenge: OtpChallenge): Promise<void>;
|
|
26
|
+
find(id: string): Promise<OtpChallenge | null>;
|
|
27
|
+
delete(id: string): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare class MemoryOtpChallengeStore implements OtpChallengeStore {
|
|
30
|
+
#private;
|
|
31
|
+
save(c: OtpChallenge): Promise<void>;
|
|
32
|
+
find(id: string): Promise<OtpChallenge | null>;
|
|
33
|
+
delete(id: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export interface OtpConfig {
|
|
36
|
+
channel: OtpDeliveryChannel;
|
|
37
|
+
/** Where pending challenges live. Default in-memory. */
|
|
38
|
+
store?: OtpChallengeStore;
|
|
39
|
+
/** Number of digits in the code. Default `6`. */
|
|
40
|
+
digits?: number;
|
|
41
|
+
/** How long a code stays valid, in seconds. Default `300` (5 min). */
|
|
42
|
+
ttlSeconds?: number;
|
|
43
|
+
/** Max verification attempts before the challenge is burned. Default `5`. */
|
|
44
|
+
maxAttempts?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface OtpStartResult {
|
|
47
|
+
challengeId: string;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
}
|
|
50
|
+
export type OtpFailureReason = "not_found" | "expired" | "too_many_attempts" | "mismatch";
|
|
51
|
+
export interface OtpVerification {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
reason?: OtpFailureReason;
|
|
54
|
+
}
|
|
55
|
+
export declare class OtpProvider {
|
|
56
|
+
#private;
|
|
57
|
+
readonly kind: "otp";
|
|
58
|
+
constructor(config: OtpConfig);
|
|
59
|
+
/** Mint a code, persist the challenge, and deliver it to `recipient`. */
|
|
60
|
+
start(recipient: string, nowMs?: number): Promise<OtpStartResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Verify a submitted code. Wrong codes consume an attempt; the challenge is
|
|
63
|
+
* deleted on success, on expiry, or once the attempt budget is exhausted.
|
|
64
|
+
*/
|
|
65
|
+
verify(challengeId: string, code: string, nowMs?: number): Promise<OtpVerification>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=OtpProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OtpProvider.d.ts","sourceRoot":"","sources":["../../src/mfa/OtpProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,gFAAgF;AAChF,MAAM,WAAW,kBAAkB;IAClC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IACjC,IAAI,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,qBAAa,uBAAwB,YAAW,iBAAiB;;IAE1D,IAAI,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAGpC,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAG9C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGvC;AAED,MAAM,WAAW,SAAS;IACzB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,wDAAwD;IACxD,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,gBAAgB,GACzB,WAAW,GACX,SAAS,GACT,mBAAmB,GACnB,UAAU,CAAC;AAEd,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED,qBAAa,WAAW;;IACvB,QAAQ,CAAC,IAAI,EAAG,KAAK,CAAU;gBAOnB,MAAM,EAAE,SAAS;IAc7B,yEAAyE;IACnE,KAAK,CACV,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,MAAmB,GACxB,OAAO,CAAC,cAAc,CAAC;IAe1B;;;OAGG;IACG,MAAM,CACX,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAmB,GACxB,OAAO,CAAC,eAAe,CAAC;CA0B3B"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtpProvider — delivered one-time passcodes (email / SMS). Unlike TOTP this is
|
|
3
|
+
* a challenge/response flow: `start()` mints a code, persists it (hashed, with
|
|
4
|
+
* an expiry and an attempt budget) and hands it to a delivery channel; the user
|
|
5
|
+
* later submits it to `verify()`.
|
|
6
|
+
*
|
|
7
|
+
* Delivery is pluggable — the app supplies an email or SMS channel. The store
|
|
8
|
+
* is pluggable too, with an in-memory default mirroring the rest of warden.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomBytes, randomInt, timingSafeEqual, } from "node:crypto";
|
|
11
|
+
import { WardenError } from "../errors.js";
|
|
12
|
+
export class MemoryOtpChallengeStore {
|
|
13
|
+
#store = new Map();
|
|
14
|
+
async save(c) {
|
|
15
|
+
this.#store.set(c.id, c);
|
|
16
|
+
}
|
|
17
|
+
async find(id) {
|
|
18
|
+
return this.#store.get(id) ?? null;
|
|
19
|
+
}
|
|
20
|
+
async delete(id) {
|
|
21
|
+
this.#store.delete(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class OtpProvider {
|
|
25
|
+
kind = "otp";
|
|
26
|
+
#channel;
|
|
27
|
+
#store;
|
|
28
|
+
#digits;
|
|
29
|
+
#ttlMs;
|
|
30
|
+
#maxAttempts;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
if (!config?.channel) {
|
|
33
|
+
throw new WardenError("INVALID_CONFIG", "OtpProvider requires a delivery channel (email/SMS)");
|
|
34
|
+
}
|
|
35
|
+
this.#channel = config.channel;
|
|
36
|
+
this.#store = config.store ?? new MemoryOtpChallengeStore();
|
|
37
|
+
this.#digits = config.digits ?? 6;
|
|
38
|
+
this.#ttlMs = (config.ttlSeconds ?? 300) * 1000;
|
|
39
|
+
this.#maxAttempts = config.maxAttempts ?? 5;
|
|
40
|
+
}
|
|
41
|
+
/** Mint a code, persist the challenge, and deliver it to `recipient`. */
|
|
42
|
+
async start(recipient, nowMs = Date.now()) {
|
|
43
|
+
const code = this.#randomCode();
|
|
44
|
+
const id = randomBytes(16).toString("hex");
|
|
45
|
+
const expiresAt = nowMs + this.#ttlMs;
|
|
46
|
+
await this.#store.save({
|
|
47
|
+
id,
|
|
48
|
+
recipient,
|
|
49
|
+
hash: saltedHash(code),
|
|
50
|
+
expiresAt,
|
|
51
|
+
attempts: 0,
|
|
52
|
+
});
|
|
53
|
+
await this.#channel.send(recipient, code);
|
|
54
|
+
return { challengeId: id, expiresAt };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Verify a submitted code. Wrong codes consume an attempt; the challenge is
|
|
58
|
+
* deleted on success, on expiry, or once the attempt budget is exhausted.
|
|
59
|
+
*/
|
|
60
|
+
async verify(challengeId, code, nowMs = Date.now()) {
|
|
61
|
+
const challenge = await this.#store.find(challengeId);
|
|
62
|
+
if (!challenge) {
|
|
63
|
+
return { ok: false, reason: "not_found" };
|
|
64
|
+
}
|
|
65
|
+
if (nowMs > challenge.expiresAt) {
|
|
66
|
+
await this.#store.delete(challengeId);
|
|
67
|
+
return { ok: false, reason: "expired" };
|
|
68
|
+
}
|
|
69
|
+
if (matchesStored(challenge.hash, code.replace(/\s/g, ""))) {
|
|
70
|
+
await this.#store.delete(challengeId);
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
const attempts = challenge.attempts + 1;
|
|
74
|
+
if (attempts >= this.#maxAttempts) {
|
|
75
|
+
await this.#store.delete(challengeId);
|
|
76
|
+
return { ok: false, reason: "too_many_attempts" };
|
|
77
|
+
}
|
|
78
|
+
await this.#store.save({ ...challenge, attempts });
|
|
79
|
+
return { ok: false, reason: "mismatch" };
|
|
80
|
+
}
|
|
81
|
+
#randomCode() {
|
|
82
|
+
const max = 10 ** this.#digits;
|
|
83
|
+
return randomInt(0, max).toString().padStart(this.#digits, "0");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function saltedHash(code) {
|
|
87
|
+
const salt = randomBytes(8);
|
|
88
|
+
const digest = createHash("sha256").update(salt).update(code).digest();
|
|
89
|
+
return `${salt.toString("hex")}$${digest.toString("hex")}`;
|
|
90
|
+
}
|
|
91
|
+
function matchesStored(stored, candidate) {
|
|
92
|
+
const sep = stored.indexOf("$");
|
|
93
|
+
if (sep === -1) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const salt = Buffer.from(stored.slice(0, sep), "hex");
|
|
97
|
+
const expected = Buffer.from(stored.slice(sep + 1), "hex");
|
|
98
|
+
const actual = createHash("sha256").update(salt).update(candidate).digest();
|
|
99
|
+
if (actual.length !== expected.length) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return timingSafeEqual(actual, expected);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=OtpProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OtpProvider.js","sourceRoot":"","sources":["../../src/mfa/OtpProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACN,UAAU,EACV,WAAW,EACX,SAAS,EACT,eAAe,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAwB3C,MAAM,OAAO,uBAAuB;IACnC,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IACzC,KAAK,CAAC,IAAI,CAAC,CAAe;QACzB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC1B,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAU;QACpB,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC;IACpC,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,EAAU;QACtB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC;CACD;AA8BD,MAAM,OAAO,WAAW;IACd,IAAI,GAAG,KAAc,CAAC;IACtB,QAAQ,CAAqB;IAC7B,MAAM,CAAoB;IAC1B,OAAO,CAAS;IAChB,MAAM,CAAS;IACf,YAAY,CAAS;IAE9B,YAAY,MAAiB;QAC5B,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YACtB,MAAM,IAAI,WAAW,CACpB,gBAAgB,EAChB,qDAAqD,CACrD,CAAC;QACH,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,IAAI,uBAAuB,EAAE,CAAC;QAC5D,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC;QAChD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,KAAK,CACV,SAAiB,EACjB,QAAgB,IAAI,CAAC,GAAG,EAAE;QAE1B,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QACtC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACtB,EAAE;YACF,SAAS;YACT,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC;YACtB,SAAS;YACT,QAAQ,EAAE,CAAC;SACX,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC1C,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CACX,WAAmB,EACnB,IAAY,EACZ,QAAgB,IAAI,CAAC,GAAG,EAAE;QAE1B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACtD,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAC3C,CAAC;QACD,IAAI,KAAK,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACzC,CAAC;QACD,IAAI,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5D,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YACtC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACrB,CAAC;QACD,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,GAAG,CAAC,CAAC;QACxC,IAAI,QAAQ,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACnC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;QACnD,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;QACnD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1C,CAAC;IAED,WAAW;QACV,MAAM,GAAG,GAAG,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC;QAC/B,OAAO,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjE,CAAC;CACD;AAED,SAAS,UAAU,CAAC,IAAY;IAC/B,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IACvE,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,SAAS,aAAa,CAAC,MAAc,EAAE,SAAiB;IACvD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;IAC5E,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TotpProvider — RFC 6238 time-based one-time passwords (HOTP/RFC 4226 under
|
|
3
|
+
* the hood). Pure TypeScript on Node's `crypto.createHmac` — no native binding
|
|
4
|
+
* and no third-party dependency: a TOTP is a single HMAC, not CPU-bound work.
|
|
5
|
+
*
|
|
6
|
+
* Compatible with Google Authenticator / 1Password / Authy by default
|
|
7
|
+
* (SHA1, 6 digits, 30s period).
|
|
8
|
+
*/
|
|
9
|
+
export type TotpAlgorithm = "SHA1" | "SHA256" | "SHA512";
|
|
10
|
+
export interface TotpConfig {
|
|
11
|
+
/** HMAC algorithm. Default `SHA1` (authenticator-app compatible). */
|
|
12
|
+
algorithm?: TotpAlgorithm;
|
|
13
|
+
/** Number of digits in the generated code. Default `6`. */
|
|
14
|
+
digits?: number;
|
|
15
|
+
/** Time step in seconds. Default `30`. */
|
|
16
|
+
period?: number;
|
|
17
|
+
/**
|
|
18
|
+
* How many time steps before/after `now` are accepted on `verify`, to
|
|
19
|
+
* tolerate clock skew. Default `1` (i.e. ±30s with the default period).
|
|
20
|
+
*/
|
|
21
|
+
window?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TotpEnrollment {
|
|
24
|
+
/** Base32 secret to persist against the user. */
|
|
25
|
+
secret: string;
|
|
26
|
+
/** `otpauth://` URI to render as a QR code during enrollment. */
|
|
27
|
+
uri: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class TotpProvider {
|
|
30
|
+
#private;
|
|
31
|
+
readonly kind: "totp";
|
|
32
|
+
constructor(config?: TotpConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Generate a fresh secret and the `otpauth://` provisioning URI. Persist
|
|
35
|
+
* `secret`; render `uri` as a QR code for the user to scan.
|
|
36
|
+
*
|
|
37
|
+
* @param account Identifies the account in the authenticator (usually the
|
|
38
|
+
* user's email or username).
|
|
39
|
+
* @param issuer Your app/brand name, shown as the entry label.
|
|
40
|
+
*/
|
|
41
|
+
enroll(account: string, issuer: string): TotpEnrollment;
|
|
42
|
+
/** Build the `otpauth://totp/...` provisioning URI for an existing secret. */
|
|
43
|
+
uri(secret: string, account: string, issuer: string): string;
|
|
44
|
+
/** Generate the code for a given secret at a given time (ms since epoch). */
|
|
45
|
+
generate(secret: string, atMs?: number): string;
|
|
46
|
+
/**
|
|
47
|
+
* Verify a user-supplied code against the secret, accepting codes from the
|
|
48
|
+
* surrounding `window` time steps to tolerate clock skew. Constant-time.
|
|
49
|
+
*/
|
|
50
|
+
verify(secret: string, code: string, atMs?: number): boolean;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=TotpProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TotpProvider.d.ts","sourceRoot":"","sources":["../../src/mfa/TotpProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,UAAU;IAC1B,qEAAqE;IACrE,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC9B,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;CACZ;AASD,qBAAa,YAAY;;IACxB,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAU;gBAGpB,MAAM,GAAE,UAAe;IAUnC;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc;IAKvD,8EAA8E;IAC9E,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAY5D,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAmB,GAAG,MAAM;IAK3D;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAmB,GAAG,OAAO;CAoCxE"}
|