@adminforth/two-factors-auth 2.10.11 → 3.0.0
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/build.log +2 -2
- package/custom/TwoFactorsConfirmation.vue +1 -0
- package/dist/custom/TwoFactorsConfirmation.vue +1 -0
- package/dist/index.js +47 -27
- package/index.ts +47 -29
- package/package.json +1 -1
- package/types.ts +8 -1
package/build.log
CHANGED
|
@@ -15,5 +15,5 @@ custom/package.json
|
|
|
15
15
|
custom/tsconfig.json
|
|
16
16
|
custom/utils.js
|
|
17
17
|
|
|
18
|
-
sent 273,
|
|
19
|
-
total size is 272,
|
|
18
|
+
sent 273,600 bytes received 210 bytes 547,620.00 bytes/sec
|
|
19
|
+
total size is 272,773 speedup is 1.00
|
|
@@ -200,6 +200,7 @@
|
|
|
200
200
|
if ( newRoute.hash === '#passkey' ) {
|
|
201
201
|
cancelPendingWebAuthn('switch-to-passkey');
|
|
202
202
|
confirmationMode.value = 'passkey';
|
|
203
|
+
usePasskeyButton();
|
|
203
204
|
} else if ( newRoute.hash === '#code' ) {
|
|
204
205
|
cancelPendingWebAuthn('switch-to-code');
|
|
205
206
|
confirmationMode.value = 'code';
|
|
@@ -200,6 +200,7 @@
|
|
|
200
200
|
if ( newRoute.hash === '#passkey' ) {
|
|
201
201
|
cancelPendingWebAuthn('switch-to-passkey');
|
|
202
202
|
confirmationMode.value = 'passkey';
|
|
203
|
+
usePasskeyButton();
|
|
203
204
|
} else if ( newRoute.hash === '#code' ) {
|
|
204
205
|
cancelPendingWebAuthn('switch-to-code');
|
|
205
206
|
confirmationMode.value = 'code';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { AdminForthPlugin, Filters, suggestIfTypo } from "adminforth";
|
|
10
|
+
import { AdminForthPlugin, Filters, suggestIfTypo, convertPeriodToSeconds } from "adminforth";
|
|
11
11
|
import twofactor from 'node-2fa';
|
|
12
12
|
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
|
|
13
13
|
import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
@@ -22,6 +22,37 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
22
22
|
instanceUniqueRepresentation(pluginOptions) {
|
|
23
23
|
return `single`;
|
|
24
24
|
}
|
|
25
|
+
useChellenge(challenge, expiresIn) {
|
|
26
|
+
const expiresInSeconds = expiresIn ? convertPeriodToSeconds(expiresIn) : undefined;
|
|
27
|
+
this.options.passkeys.keyValueAdapter.set(challenge, 'stub_value', expiresInSeconds);
|
|
28
|
+
}
|
|
29
|
+
checkIfChellengeNotUsed(challenge) {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
const res = yield this.options.passkeys.keyValueAdapter.get(challenge);
|
|
32
|
+
if (!res) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
validateCookiesForPasskeyLogin(cookies) {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
var _a;
|
|
41
|
+
const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyLoginTemporaryJWT` });
|
|
42
|
+
if (!passkeysCookies) {
|
|
43
|
+
return { ok: false, error: 'Passkey token is required' };
|
|
44
|
+
}
|
|
45
|
+
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
46
|
+
const isChallangeValid = yield this.checkIfChellengeNotUsed(decodedPasskeysCookies.challenge);
|
|
47
|
+
if (isChallangeValid) {
|
|
48
|
+
this.useChellenge(decodedPasskeysCookies.challenge, ((_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.challengeValidityPeriod) || '2m');
|
|
49
|
+
}
|
|
50
|
+
if (!decodedPasskeysCookies || !isChallangeValid) {
|
|
51
|
+
return { ok: false, error: 'Invalid passkey' };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, decodedPasskeysCookies };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
25
56
|
checkIfSkipSetupAllowSkipVerify(adminUser) {
|
|
26
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
27
58
|
if (this.options.usersFilterToAllowSkipSetup) {
|
|
@@ -164,17 +195,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
164
195
|
;
|
|
165
196
|
}
|
|
166
197
|
else if (confirmationResult.mode === "passkey") {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!passkeysCookies) {
|
|
171
|
-
return { error: 'Passkey token is required' };
|
|
172
|
-
}
|
|
173
|
-
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
174
|
-
if (!decodedPasskeysCookies) {
|
|
175
|
-
return { error: 'Invalid passkey' };
|
|
198
|
+
const cookiesValidationResult = yield this.validateCookiesForPasskeyLogin(cookies);
|
|
199
|
+
if (!cookiesValidationResult.ok) {
|
|
200
|
+
return { error: cookiesValidationResult.error };
|
|
176
201
|
}
|
|
177
|
-
const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies);
|
|
202
|
+
const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
178
203
|
if (verificationResult.ok && verificationResult.passkeyConfirmed) {
|
|
179
204
|
//* SET GRACE COOKIE *//
|
|
180
205
|
if (this.options.stepUpMfaGracePeriodSeconds) {
|
|
@@ -325,6 +350,9 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
325
350
|
if (!this.options.passkeys.credentialIdFieldName) {
|
|
326
351
|
throw new Error('Passkeys credentialIdFieldName is required');
|
|
327
352
|
}
|
|
353
|
+
if (!this.options.passkeys.keyValueAdapter) {
|
|
354
|
+
throw new Error('Passkeys keyValueAdapter is required');
|
|
355
|
+
}
|
|
328
356
|
const credentialResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
329
357
|
const credentialIDField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialIdFieldName);
|
|
330
358
|
if (!credentialIDField) {
|
|
@@ -490,15 +518,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
490
518
|
let verified = null;
|
|
491
519
|
if (body.usePasskey && this.options.passkeys) {
|
|
492
520
|
// passkeys are enabled and user wants to use them
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
495
|
-
return { error:
|
|
496
|
-
}
|
|
497
|
-
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
498
|
-
if (!decodedPasskeysCookies) {
|
|
499
|
-
return { error: 'Invalid passkey' };
|
|
521
|
+
const cookiesValidationResult = yield this.validateCookiesForPasskeyLogin(cookies);
|
|
522
|
+
if (!cookiesValidationResult.ok) {
|
|
523
|
+
return { error: cookiesValidationResult.error };
|
|
500
524
|
}
|
|
501
|
-
const res = yield this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
|
|
525
|
+
const res = yield this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, cookiesValidationResult.decodedPasskeysCookies);
|
|
502
526
|
if (res.ok && res.passkeyConfirmed) {
|
|
503
527
|
verified = true;
|
|
504
528
|
}
|
|
@@ -533,13 +557,9 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
533
557
|
if (!passkeyResponse) {
|
|
534
558
|
return { error: 'Passkey response is required' };
|
|
535
559
|
}
|
|
536
|
-
const
|
|
537
|
-
if (!
|
|
538
|
-
return { error:
|
|
539
|
-
}
|
|
540
|
-
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'tempLoginPasskeyChallenge', false);
|
|
541
|
-
if (!decoded) {
|
|
542
|
-
return { error: 'Authentication session is expired. Please, try again' };
|
|
560
|
+
const cookiesValidationResult = yield this.validateCookiesForPasskeyLogin(cookies);
|
|
561
|
+
if (!cookiesValidationResult.ok) {
|
|
562
|
+
return { error: cookiesValidationResult.error };
|
|
543
563
|
}
|
|
544
564
|
let parsedPasskeyResponse;
|
|
545
565
|
try {
|
|
@@ -568,7 +588,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
568
588
|
if (!user) {
|
|
569
589
|
return { error: 'User not found' };
|
|
570
590
|
}
|
|
571
|
-
const verificationResult = yield this.verifyPasskeyResponse(passkeyResponse, userPk,
|
|
591
|
+
const verificationResult = yield this.verifyPasskeyResponse(passkeyResponse, userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
572
592
|
if (!verificationResult.ok || !verificationResult.passkeyConfirmed) {
|
|
573
593
|
return { error: 'Passkey verification failed' };
|
|
574
594
|
}
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminForthPlugin, Filters, suggestIfTypo, HttpExtra } from "adminforth";
|
|
1
|
+
import { AdminForthPlugin, Filters, suggestIfTypo, HttpExtra, convertPeriodToSeconds } from "adminforth";
|
|
2
2
|
import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, BeforeLoginConfirmationFunction, IAdminForthHttpResponse } from "adminforth";
|
|
3
3
|
import twofactor from 'node-2fa';
|
|
4
4
|
import { PluginOptions } from "./types.js"
|
|
@@ -29,6 +29,36 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
29
29
|
return `single`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
private useChellenge(challenge: string, expiresIn?: string): void {
|
|
33
|
+
const expiresInSeconds = expiresIn ? convertPeriodToSeconds(expiresIn) : undefined;
|
|
34
|
+
this.options.passkeys.keyValueAdapter.set(challenge, 'stub_value', expiresInSeconds);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async checkIfChellengeNotUsed(challenge: string): Promise<boolean> {
|
|
38
|
+
const res = await this.options.passkeys.keyValueAdapter.get(challenge);
|
|
39
|
+
if (!res) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async validateCookiesForPasskeyLogin(cookies: any): Promise<{ok: boolean, decodedPasskeysCookies?: any, error?: string}> {
|
|
46
|
+
const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`});
|
|
47
|
+
if (!passkeysCookies) {
|
|
48
|
+
return { ok: false, error: 'Passkey token is required' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
52
|
+
const isChallangeValid = await this.checkIfChellengeNotUsed(decodedPasskeysCookies.challenge);
|
|
53
|
+
if (isChallangeValid) {
|
|
54
|
+
this.useChellenge(decodedPasskeysCookies.challenge, this.options.passkeys?.challengeValidityPeriod || '2m');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!decodedPasskeysCookies || !isChallangeValid) {
|
|
58
|
+
return { ok: false, error: 'Invalid passkey' };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, decodedPasskeysCookies };
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
public async checkIfSkipSetupAllowSkipVerify(adminUser: AdminUser): Promise<{ skipAllowed: boolean }> {
|
|
34
64
|
if (this.options.usersFilterToAllowSkipSetup) {
|
|
@@ -171,18 +201,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
171
201
|
return { error: "Wrong or expired OTP code" }
|
|
172
202
|
};
|
|
173
203
|
} else if (confirmationResult.mode === "passkey") {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!passkeysCookies) {
|
|
178
|
-
return { error: 'Passkey token is required' };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
182
|
-
if (!decodedPasskeysCookies) {
|
|
183
|
-
return { error: 'Invalid passkey' };
|
|
204
|
+
const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies);
|
|
205
|
+
if (!cookiesValidationResult.ok) {
|
|
206
|
+
return { error: cookiesValidationResult.error };
|
|
184
207
|
}
|
|
185
|
-
const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies );
|
|
208
|
+
const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, cookiesValidationResult.decodedPasskeysCookies );
|
|
186
209
|
|
|
187
210
|
if (verificationResult.ok && verificationResult.passkeyConfirmed) {
|
|
188
211
|
|
|
@@ -340,6 +363,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
340
363
|
throw new Error('Passkeys credentialIdFieldName is required');
|
|
341
364
|
}
|
|
342
365
|
|
|
366
|
+
if (!this.options.passkeys.keyValueAdapter) {
|
|
367
|
+
throw new Error('Passkeys keyValueAdapter is required');
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
const credentialResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
344
371
|
const credentialIDField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialIdFieldName);
|
|
345
372
|
if ( !credentialIDField ) {
|
|
@@ -529,15 +556,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
529
556
|
let verified = null;
|
|
530
557
|
if (body.usePasskey && this.options.passkeys) {
|
|
531
558
|
// passkeys are enabled and user wants to use them
|
|
532
|
-
const
|
|
533
|
-
if (!
|
|
534
|
-
return { error:
|
|
559
|
+
const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies);
|
|
560
|
+
if (!cookiesValidationResult.ok) {
|
|
561
|
+
return { error: cookiesValidationResult.error };
|
|
535
562
|
}
|
|
536
|
-
const
|
|
537
|
-
if (!decodedPasskeysCookies) {
|
|
538
|
-
return { error: 'Invalid passkey' };
|
|
539
|
-
}
|
|
540
|
-
const res = await this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
|
|
563
|
+
const res = await this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, cookiesValidationResult.decodedPasskeysCookies);
|
|
541
564
|
if (res.ok && res.passkeyConfirmed) {
|
|
542
565
|
verified = true;
|
|
543
566
|
}
|
|
@@ -572,14 +595,9 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
572
595
|
return { error: 'Passkey response is required' };
|
|
573
596
|
}
|
|
574
597
|
|
|
575
|
-
const
|
|
576
|
-
if (!
|
|
577
|
-
return { error:
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const decoded = await this.adminforth.auth.verify(totpTemporaryJWT, 'tempLoginPasskeyChallenge', false);
|
|
581
|
-
if (!decoded) {
|
|
582
|
-
return { error: 'Authentication session is expired. Please, try again' }
|
|
598
|
+
const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies);
|
|
599
|
+
if (!cookiesValidationResult.ok) {
|
|
600
|
+
return { error: cookiesValidationResult.error };
|
|
583
601
|
}
|
|
584
602
|
|
|
585
603
|
let parsedPasskeyResponse;
|
|
@@ -612,7 +630,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
612
630
|
return { error: 'User not found' };
|
|
613
631
|
}
|
|
614
632
|
|
|
615
|
-
const verificationResult = await this.verifyPasskeyResponse(passkeyResponse, userPk,
|
|
633
|
+
const verificationResult = await this.verifyPasskeyResponse(passkeyResponse, userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
616
634
|
if (!verificationResult.ok || !verificationResult.passkeyConfirmed) {
|
|
617
635
|
return { error: 'Passkey verification failed' };
|
|
618
636
|
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminUser } from "adminforth";
|
|
1
|
+
import { AdminUser, KeyValueAdapter } from "adminforth";
|
|
2
2
|
|
|
3
3
|
export type PluginOptions = {
|
|
4
4
|
|
|
@@ -33,6 +33,13 @@ export type PluginOptions = {
|
|
|
33
33
|
credentialMetaFieldName: string,
|
|
34
34
|
credentialUserIdFieldName: string,
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* KeyValueAdapter is required to make sure that generated challenge can't be reused more than once
|
|
39
|
+
*/
|
|
40
|
+
keyValueAdapter: KeyValueAdapter,
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* Allow login with Passkeys even if 2FA is not set up. Default is true.
|
|
38
45
|
*/
|