@adminforth/two-factors-auth 2.9.20 → 2.10.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.
@@ -4,8 +4,10 @@ set -x
4
4
 
5
5
  COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
6
6
 
7
+ STATUS=${1}
7
8
 
8
- if [ "$CI_PREV_PIPELINE_STATUS" = "success" ]; then
9
+
10
+ if [ "$STATUS" = "success" ]; then
9
11
  MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
10
12
 
11
13
  curl -s -X POST -H "Content-Type: application/json" -d '{
@@ -16,25 +16,42 @@ steps:
16
16
  commands:
17
17
  - infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
18
18
 
19
- release:
19
+ build:
20
20
  image: node:20
21
21
  when:
22
22
  - event: push
23
23
  commands:
24
24
  - apt update && apt install -y rsync
25
- - export $(cat /woodpecker/deploy.vault.env | xargs)
25
+ - . /woodpecker/deploy.vault.env
26
26
  - npm clean-install
27
27
  - /bin/bash ./.woodpecker/buildRelease.sh
28
28
  - npm audit signatures
29
+
30
+ release:
31
+ image: node:20
32
+ when:
33
+ - event:
34
+ - push
35
+ branch:
36
+ - main
37
+ commands:
38
+ - . /woodpecker/deploy.vault.env
29
39
  - npx semantic-release
30
40
 
31
41
  slack-on-failure:
42
+ image: curlimages/curl
32
43
  when:
33
44
  - event: push
34
- status: [failure, success]
35
- - event: push
45
+ status: [failure]
46
+ commands:
47
+ - . /woodpecker/deploy.vault.env
48
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh failure
49
+
50
+ slack-on-success:
36
51
  image: curlimages/curl
52
+ when:
53
+ - event: push
54
+ status: [success]
37
55
  commands:
38
- - export $(cat /woodpecker/deploy.vault.env | xargs)
39
- - /bin/sh ./.woodpecker/buildSlackNotify.sh
40
-
56
+ - . /woodpecker/deploy.vault.env
57
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh success
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,593 bytes received 210 bytes 547,606.00 bytes/sec
18
+ sent 273,582 bytes received 210 bytes 547,584.00 bytes/sec
19
19
  total size is 272,766 speedup is 1.00
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import twofactor from 'node-2fa';
12
12
  import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
13
13
  import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
14
14
  import aaguids from './custom/aaguid.json';
15
+ import crypto from 'crypto';
15
16
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
16
17
  constructor(options) {
17
18
  super(options, import.meta.url);
@@ -45,9 +46,63 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
45
46
  return { skipAllowed: false };
46
47
  });
47
48
  }
49
+ generateHashForStepUpMfaGraceCookie(headers) {
50
+ const ip = this.adminforth.auth.getClientIp(headers);
51
+ const userAgent = headers['user-agent'] || '';
52
+ const acceptLanguage = headers['accept-language'] || '';
53
+ if (!ip || !userAgent || !acceptLanguage) {
54
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: missing required request headers to identify client ❗️❗️❗️");
55
+ return null;
56
+ }
57
+ else {
58
+ const hmac = crypto.createHmac('sha256', process.env.ADMINFORTH_SECRET)
59
+ .update(`${acceptLanguage}_${userAgent}_${ip}`)
60
+ .digest('hex');
61
+ return hmac;
62
+ }
63
+ }
64
+ issueTempSkip2FAGraceJWT(opts) {
65
+ if (opts.response) {
66
+ if (opts.extra.headers) {
67
+ const hash = this.generateHashForStepUpMfaGraceCookie(opts.extra.headers);
68
+ if (!hash) {
69
+ return;
70
+ }
71
+ const jwt = this.adminforth.auth.issueJWT({ hash: hash }, 'MfaGrace', `${this.options.stepUpMfaGracePeriodSeconds}s`);
72
+ //TODO: fix ts-ignore after releasing new version of adminforth with updated types
73
+ //@ts-ignore
74
+ this.adminforth.auth.setCustomCookie({ response: opts.response, payload: { name: "TempSkip2FA_Modal_JWT", value: jwt, expirySeconds: this.options.stepUpMfaGracePeriodSeconds, httpOnly: true } });
75
+ }
76
+ }
77
+ else {
78
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: response object is missing. You probably called verify() method without response parameter ❗️❗️❗️");
79
+ }
80
+ }
81
+ isTempSkip2FAGraceValid(headers_1, cookies_1) {
82
+ return __awaiter(this, arguments, void 0, function* (headers, cookies, checkIfJWTAboutToExpire = false) {
83
+ const hash = this.generateHashForStepUpMfaGraceCookie(headers);
84
+ if (!hash) {
85
+ return false;
86
+ }
87
+ const jwt = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "TempSkip2FA_Modal_JWT" });
88
+ const jwtVerificationResult = yield this.adminforth.auth.verify(jwt, 'MfaGrace', false);
89
+ if (!jwtVerificationResult) {
90
+ return false;
91
+ }
92
+ const jwtHash = jwtVerificationResult['hash'];
93
+ if (checkIfJWTAboutToExpire && (jwtVerificationResult["exp"] - (Date.now() / 1000)) < 30) {
94
+ console.error("❗️❗️❗️ Cannot validate step-up MFA grace cookie: token is expired or about to expire ❗️❗️❗️");
95
+ return false;
96
+ }
97
+ if (hash === jwtHash) {
98
+ return true;
99
+ }
100
+ return false;
101
+ });
102
+ }
48
103
  verify(confirmationResult, opts) {
49
104
  return __awaiter(this, void 0, void 0, function* () {
50
- var _a, _b, _c, _d;
105
+ var _a, _b, _c, _d, _e;
51
106
  if (!confirmationResult)
52
107
  return { error: "Confirmation result is required" };
53
108
  if (this.options.usersFilterToApply) {
@@ -62,6 +117,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
62
117
  return { ok: true };
63
118
  }
64
119
  }
120
+ if (this.options.stepUpMfaGracePeriodSeconds && ((_a = opts.extra) === null || _a === void 0 ? void 0 : _a.headers) && !confirmationResult.mode) {
121
+ const verificationResult = yield this.isTempSkip2FAGraceValid(opts.extra.headers, opts.cookies);
122
+ if (verificationResult === true) {
123
+ return { ok: true };
124
+ }
125
+ }
65
126
  if (confirmationResult.mode === "totp") {
66
127
  const code = confirmationResult.result;
67
128
  const authRes = this.adminforth.config.resources
@@ -69,10 +130,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
69
130
  if (!authRes)
70
131
  return { error: "Auth resource not found" };
71
132
  const connector = this.adminforth.connectors[authRes.dataSource];
72
- const pkName = (_a = authRes.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name;
133
+ const pkName = (_b = authRes.columns.find(c => c.primaryKey)) === null || _b === void 0 ? void 0 : _b.name;
73
134
  if (!pkName)
74
135
  return { error: "Primary key not found on auth resource" };
75
- const pk = (_b = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _b !== void 0 ? _b : (_d = (_c = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _c === void 0 ? void 0 : _c.dbUser) === null || _d === void 0 ? void 0 : _d[pkName];
136
+ const pk = (_c = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _c !== void 0 ? _c : (_e = (_d = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _d === void 0 ? void 0 : _d.dbUser) === null || _e === void 0 ? void 0 : _e[pkName];
76
137
  if (!pk)
77
138
  return { error: "User PK is required" };
78
139
  const user = yield connector.getRecordByPrimaryKey(authRes, pk);
@@ -82,7 +143,17 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
82
143
  if (!secret)
83
144
  return { error: "2FA is not set up for this user" };
84
145
  const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
85
- return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
146
+ //* SET GRACE COOKIE *//
147
+ if (verified) {
148
+ if (this.options.stepUpMfaGracePeriodSeconds) {
149
+ this.issueTempSkip2FAGraceJWT(opts);
150
+ }
151
+ return { ok: true };
152
+ }
153
+ else {
154
+ return { error: "Wrong or expired OTP code" };
155
+ }
156
+ ;
86
157
  }
87
158
  else if (confirmationResult.mode === "passkey") {
88
159
  const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: opts.cookies, name: `passkeyLoginTemporaryJWT` });
@@ -95,6 +166,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
95
166
  }
96
167
  const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies);
97
168
  if (verificationResult.ok && verificationResult.passkeyConfirmed) {
169
+ //* SET GRACE COOKIE *//
170
+ if (this.options.stepUpMfaGracePeriodSeconds) {
171
+ this.issueTempSkip2FAGraceJWT(opts);
172
+ }
98
173
  return { ok: true };
99
174
  }
100
175
  return { error: "Invalid passkey" };
@@ -543,7 +618,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
543
618
  server.endpoint({
544
619
  method: "GET",
545
620
  path: "/plugin/twofa/skip-allow-modal",
546
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
621
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, headers, cookies }) {
547
622
  if (this.options.usersFilterToApply) {
548
623
  const res = this.options.usersFilterToApply(adminUser);
549
624
  if (res === false) {
@@ -556,6 +631,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
556
631
  return { skipAllowed: true };
557
632
  }
558
633
  }
634
+ if (this.options.stepUpMfaGracePeriodSeconds) {
635
+ const verificationResult = yield this.isTempSkip2FAGraceValid(headers, cookies, true);
636
+ if (verificationResult === true) {
637
+ return { skipAllowed: true };
638
+ }
639
+ }
559
640
  return { skipAllowed: false };
560
641
  }),
561
642
  });
@@ -582,14 +663,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
582
663
  method: 'POST',
583
664
  path: `/plugin/passkeys/registerPasskeyRequest`,
584
665
  noAuth: false,
585
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
666
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, extra }) {
586
667
  var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
587
668
  const mode = body === null || body === void 0 ? void 0 : body.mode;
588
669
  const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
589
670
  const verificationResult = yield this.verify(confirmationResult, {
590
671
  adminUser: adminUser,
591
672
  userPk: adminUser.pk,
592
- cookies: cookies
673
+ cookies: cookies,
674
+ response: response,
675
+ extra: Object.assign({}, extra)
593
676
  });
594
677
  if (!verificationResult || !('ok' in verificationResult)) {
595
678
  return { ok: false, error: 'Verification failed' };
package/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from '@simplewebauthn/server';
11
11
  import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
12
12
  import aaguids from './custom/aaguid.json';
13
+ import crypto from 'crypto';
13
14
 
14
15
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
15
16
  options: PluginOptions;
@@ -54,9 +55,62 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
54
55
  return { skipAllowed: false };
55
56
  }
56
57
 
58
+ private generateHashForStepUpMfaGraceCookie(headers): string {
59
+ const ip = this.adminforth.auth.getClientIp(headers);
60
+ const userAgent = headers['user-agent'] || '';
61
+ const acceptLanguage = headers['accept-language'] || '';
62
+ if (!ip || !userAgent || !acceptLanguage) {
63
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: missing required request headers to identify client ❗️❗️❗️");
64
+ return null;
65
+ } else {
66
+ const hmac = crypto.createHmac('sha256', process.env.ADMINFORTH_SECRET)
67
+ .update(`${acceptLanguage}_${userAgent}_${ip}`)
68
+ .digest('hex');
69
+ return hmac;
70
+ }
71
+ }
72
+
73
+ private issueTempSkip2FAGraceJWT(opts): void {
74
+ if (opts.response) {
75
+ if (opts.extra.headers) {
76
+ const hash = this.generateHashForStepUpMfaGraceCookie(opts.extra.headers);
77
+ if (!hash) {
78
+ return;
79
+ }
80
+ const jwt = this.adminforth.auth.issueJWT({ hash: hash }, 'MfaGrace', `${this.options.stepUpMfaGracePeriodSeconds}s`);
81
+ //TODO: fix ts-ignore after releasing new version of adminforth with updated types
82
+ //@ts-ignore
83
+ this.adminforth.auth.setCustomCookie({response: opts.response, payload: {name: "TempSkip2FA_Modal_JWT", value: jwt, expirySeconds: this.options.stepUpMfaGracePeriodSeconds, httpOnly: true}});
84
+ }
85
+ } else {
86
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: response object is missing. You probably called verify() method without response parameter ❗️❗️❗️");
87
+ }
88
+ }
89
+
90
+ private async isTempSkip2FAGraceValid(headers, cookies, checkIfJWTAboutToExpire: boolean = false): Promise<boolean> {
91
+ const hash = this.generateHashForStepUpMfaGraceCookie(headers);
92
+ if (!hash) {
93
+ return false;
94
+ }
95
+ const jwt = this.adminforth.auth.getCustomCookie({cookies: cookies, name: "TempSkip2FA_Modal_JWT"});
96
+ const jwtVerificationResult = await this.adminforth.auth.verify(jwt, 'MfaGrace', false)
97
+ if (!jwtVerificationResult) {
98
+ return false;
99
+ }
100
+ const jwtHash = jwtVerificationResult['hash'];
101
+ if (checkIfJWTAboutToExpire && (jwtVerificationResult["exp"] - ( Date.now() / 1000)) < 30 ) {
102
+ console.error("❗️❗️❗️ Cannot validate step-up MFA grace cookie: token is expired or about to expire ❗️❗️❗️");
103
+ return false;
104
+ }
105
+ if (hash === jwtHash) {
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+
57
111
  public async verify(
58
112
  confirmationResult: Record<string, any>,
59
- opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any }
113
+ opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any, response?: IAdminForthHttpResponse, extra?: HttpExtra }
60
114
  ): Promise<{ ok: true } | { error: string }> {
61
115
  if (!confirmationResult) return { error: "Confirmation result is required" };
62
116
  if (this.options.usersFilterToApply) {
@@ -71,6 +125,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
71
125
  return { ok: true };
72
126
  }
73
127
  }
128
+ if ( this.options.stepUpMfaGracePeriodSeconds && opts.extra?.headers && !confirmationResult.mode) {
129
+ const verificationResult = await this.isTempSkip2FAGraceValid(opts.extra.headers, opts.cookies);
130
+ if ( verificationResult === true ) {
131
+ return { ok: true };
132
+ }
133
+ }
134
+
74
135
  if (confirmationResult.mode === "totp") {
75
136
  const code = confirmationResult.result;
76
137
  const authRes = this.adminforth.config.resources
@@ -92,7 +153,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
92
153
  if (!secret) return { error: "2FA is not set up for this user" };
93
154
 
94
155
  const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
95
- return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
156
+
157
+ //* SET GRACE COOKIE *//
158
+ if ( verified ) {
159
+ if (this.options.stepUpMfaGracePeriodSeconds) {
160
+ this.issueTempSkip2FAGraceJWT(opts);
161
+ }
162
+ return { ok: true }
163
+ } else {
164
+ return { error: "Wrong or expired OTP code" }
165
+ };
96
166
  } else if (confirmationResult.mode === "passkey") {
97
167
  const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: opts.cookies, name: `passkeyLoginTemporaryJWT`});
98
168
  if (!passkeysCookies) {
@@ -106,6 +176,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
106
176
  const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies );
107
177
 
108
178
  if (verificationResult.ok && verificationResult.passkeyConfirmed) {
179
+
180
+ //* SET GRACE COOKIE *//
181
+ if (this.options.stepUpMfaGracePeriodSeconds) {
182
+ this.issueTempSkip2FAGraceJWT(opts);
183
+ }
109
184
  return { ok: true }
110
185
  }
111
186
  return { error: "Invalid passkey" }
@@ -601,7 +676,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
601
676
  server.endpoint({
602
677
  method: "GET",
603
678
  path: "/plugin/twofa/skip-allow-modal",
604
- handler: async ({ adminUser }) => {
679
+ handler: async ({ adminUser, headers, cookies }) => {
605
680
  if ( this.options.usersFilterToApply ) {
606
681
  const res = this.options.usersFilterToApply(adminUser);
607
682
  if ( res === false ) {
@@ -614,6 +689,14 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
614
689
  return { skipAllowed: true };
615
690
  }
616
691
  }
692
+
693
+ if ( this.options.stepUpMfaGracePeriodSeconds ) {
694
+ const verificationResult = await this.isTempSkip2FAGraceValid(headers, cookies, true);
695
+ if ( verificationResult === true ) {
696
+ return { skipAllowed: true };
697
+ }
698
+ }
699
+
617
700
  return { skipAllowed: false };
618
701
  },
619
702
  });
@@ -641,14 +724,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
641
724
  method: 'POST',
642
725
  path: `/plugin/passkeys/registerPasskeyRequest`,
643
726
  noAuth: false,
644
- handler: async ({ body, adminUser, response, cookies }) => {
727
+ handler: async ({ body, adminUser, response, cookies, extra }) => {
645
728
  const mode = body?.mode;
646
729
 
647
730
  const confirmationResult = body?.confirmationResult;
648
731
  const verificationResult = await this.verify(confirmationResult, {
649
732
  adminUser: adminUser,
650
733
  userPk: adminUser.pk,
651
- cookies: cookies
734
+ cookies: cookies,
735
+ response: response,
736
+ extra: { ...extra }
652
737
  });
653
738
  if ( !verificationResult || !('ok' in verificationResult) ) {
654
739
  return { ok: false, error: 'Verification failed' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "2.9.20",
3
+ "version": "2.10.0",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/types.ts CHANGED
@@ -16,6 +16,12 @@ export type PluginOptions = {
16
16
  */
17
17
  timeStepWindow?: number;
18
18
 
19
+ /**
20
+ * Number of seconds for step-up MFA grace period.
21
+ * During this period, user will not be challenged with 2FA on sensitive actions.
22
+ */
23
+ stepUpMfaGracePeriodSeconds?: number;
24
+
19
25
  customBrandPrefix?: string;
20
26
 
21
27
  /**