@adminforth/two-factors-auth 2.10.12 → 3.0.1

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.
@@ -17,18 +17,18 @@ steps:
17
17
  - infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
18
18
 
19
19
  build:
20
- image: node:20
20
+ image: devforth/node20-pnpm:latest
21
21
  when:
22
22
  - event: push
23
23
  commands:
24
24
  - apt update && apt install -y rsync
25
25
  - . /woodpecker/deploy.vault.env
26
- - npm clean-install
26
+ - pnpm install
27
27
  - /bin/bash ./.woodpecker/buildRelease.sh
28
28
  - npm audit signatures
29
29
 
30
30
  release:
31
- image: node:20
31
+ image: devforth/node20-pnpm:latest
32
32
  when:
33
33
  - event:
34
34
  - push
@@ -36,7 +36,7 @@ steps:
36
36
  - main
37
37
  commands:
38
38
  - . /woodpecker/deploy.vault.env
39
- - npx semantic-release
39
+ - pnpm exec semantic-release
40
40
 
41
41
  slack-on-failure:
42
42
  image: curlimages/curl
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
- //TODO: fix ts-ignore after releasing new version of adminforth with updated types
168
- //@ts-ignore
169
- const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyLoginTemporaryJWT` });
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 passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyLoginTemporaryJWT` });
494
- if (!passkeysCookies) {
495
- return { error: 'Passkey token is required' };
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 totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "passkeyLoginTemporaryJWT" });
537
- if (!totpTemporaryJWT) {
538
- return { error: 'Authentication session is expired. Please, try again' };
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, decoded);
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
- //TODO: fix ts-ignore after releasing new version of adminforth with updated types
175
- //@ts-ignore
176
- const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`});
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 passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`});
533
- if (!passkeysCookies) {
534
- return { error: 'Passkey token is required' };
559
+ const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies);
560
+ if (!cookiesValidationResult.ok) {
561
+ return { error: cookiesValidationResult.error };
535
562
  }
536
- const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
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 totpTemporaryJWT = this.adminforth.auth.getCustomCookie({cookies: cookies, name: "passkeyLoginTemporaryJWT"});
576
- if (!totpTemporaryJWT) {
577
- return { error: 'Authentication session is expired. Please, try again' }
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, decoded);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "2.10.12",
3
+ "version": "3.0.1",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,7 +14,6 @@
14
14
  },
15
15
  "type": "module",
16
16
  "scripts": {
17
- "prepare": "npm link adminforth",
18
17
  "build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
19
18
  },
20
19
  "keywords": [
@@ -29,7 +28,7 @@
29
28
  "node-2fa": "^2.0.3"
30
29
  },
31
30
  "peerDependencies": {
32
- "adminforth": "next"
31
+ "adminforth": "^2.24.0"
33
32
  },
34
33
  "devDependencies": {
35
34
  "@types/node": "^22.10.7",
@@ -46,6 +45,7 @@
46
45
  [
47
46
  "semantic-release-slack-bot",
48
47
  {
48
+ "packageName": "@adminforth/two-factors-auth",
49
49
  "notifyOnSuccess": true,
50
50
  "notifyOnFail": true,
51
51
  "slackIcon": ":package:",
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
  */