@adminforth/two-factors-auth 2.9.21 → 2.10.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.
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,64 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
45
46
  return { skipAllowed: false };
46
47
  });
47
48
  }
49
+ generateHashForStepUpMfaGraceCookie(headers, cookies) {
50
+ const ip = this.adminforth.auth.getClientIp(headers);
51
+ const userAgent = headers['user-agent'] || '';
52
+ const acceptLanguage = headers['accept-language'] || '';
53
+ const session_cookie = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "jwt" });
54
+ if (!ip || !userAgent || !acceptLanguage || !session_cookie) {
55
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: missing required request headers to identify client ❗️❗️❗️");
56
+ return null;
57
+ }
58
+ else {
59
+ const hmac = crypto.createHmac('sha256', process.env.ADMINFORTH_SECRET)
60
+ .update(`${acceptLanguage}_${userAgent}_${ip}_${session_cookie}`)
61
+ .digest('hex');
62
+ return hmac;
63
+ }
64
+ }
65
+ issueTempSkip2FAGraceJWT(opts) {
66
+ if (opts.response) {
67
+ if (opts.extra.headers) {
68
+ const hash = this.generateHashForStepUpMfaGraceCookie(opts.extra.headers, opts.cookies);
69
+ if (!hash) {
70
+ return;
71
+ }
72
+ const jwt = this.adminforth.auth.issueJWT({ hash: hash }, 'MfaGrace', `${this.options.stepUpMfaGracePeriodSeconds}s`);
73
+ //TODO: fix ts-ignore after releasing new version of adminforth with updated types
74
+ //@ts-ignore
75
+ this.adminforth.auth.setCustomCookie({ response: opts.response, payload: { name: "TempSkip2FA_Modal_JWT", value: jwt, expirySeconds: this.options.stepUpMfaGracePeriodSeconds, httpOnly: true } });
76
+ }
77
+ }
78
+ else {
79
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: response object is missing. You probably called verify() method without response parameter ❗️❗️❗️");
80
+ }
81
+ }
82
+ isTempSkip2FAGraceValid(headers_1, cookies_1) {
83
+ return __awaiter(this, arguments, void 0, function* (headers, cookies, checkIfJWTAboutToExpire = false) {
84
+ const hash = this.generateHashForStepUpMfaGraceCookie(headers, cookies);
85
+ if (!hash) {
86
+ return false;
87
+ }
88
+ const jwt = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "TempSkip2FA_Modal_JWT" });
89
+ const jwtVerificationResult = yield this.adminforth.auth.verify(jwt, 'MfaGrace', false);
90
+ if (!jwtVerificationResult) {
91
+ return false;
92
+ }
93
+ const jwtHash = jwtVerificationResult['hash'];
94
+ if (checkIfJWTAboutToExpire && (jwtVerificationResult["exp"] - (Date.now() / 1000)) < 30) {
95
+ console.error("❗️❗️❗️ Cannot validate step-up MFA grace cookie: token is expired or about to expire ❗️❗️❗️");
96
+ return false;
97
+ }
98
+ if (hash === jwtHash) {
99
+ return true;
100
+ }
101
+ return false;
102
+ });
103
+ }
48
104
  verify(confirmationResult, opts) {
49
105
  return __awaiter(this, void 0, void 0, function* () {
50
- var _a, _b, _c, _d;
106
+ var _a, _b, _c, _d, _e;
51
107
  if (!confirmationResult)
52
108
  return { error: "Confirmation result is required" };
53
109
  if (this.options.usersFilterToApply) {
@@ -62,6 +118,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
62
118
  return { ok: true };
63
119
  }
64
120
  }
121
+ if (this.options.stepUpMfaGracePeriodSeconds && ((_a = opts.extra) === null || _a === void 0 ? void 0 : _a.headers) && !confirmationResult.mode) {
122
+ const verificationResult = yield this.isTempSkip2FAGraceValid(opts.extra.headers, opts.cookies);
123
+ if (verificationResult === true) {
124
+ return { ok: true };
125
+ }
126
+ }
65
127
  if (confirmationResult.mode === "totp") {
66
128
  const code = confirmationResult.result;
67
129
  const authRes = this.adminforth.config.resources
@@ -69,10 +131,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
69
131
  if (!authRes)
70
132
  return { error: "Auth resource not found" };
71
133
  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;
134
+ const pkName = (_b = authRes.columns.find(c => c.primaryKey)) === null || _b === void 0 ? void 0 : _b.name;
73
135
  if (!pkName)
74
136
  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];
137
+ 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
138
  if (!pk)
77
139
  return { error: "User PK is required" };
78
140
  const user = yield connector.getRecordByPrimaryKey(authRes, pk);
@@ -82,7 +144,17 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
82
144
  if (!secret)
83
145
  return { error: "2FA is not set up for this user" };
84
146
  const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
85
- return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
147
+ //* SET GRACE COOKIE *//
148
+ if (verified) {
149
+ if (this.options.stepUpMfaGracePeriodSeconds) {
150
+ this.issueTempSkip2FAGraceJWT(opts);
151
+ }
152
+ return { ok: true };
153
+ }
154
+ else {
155
+ return { error: "Wrong or expired OTP code" };
156
+ }
157
+ ;
86
158
  }
87
159
  else if (confirmationResult.mode === "passkey") {
88
160
  const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: opts.cookies, name: `passkeyLoginTemporaryJWT` });
@@ -95,6 +167,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
95
167
  }
96
168
  const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies);
97
169
  if (verificationResult.ok && verificationResult.passkeyConfirmed) {
170
+ //* SET GRACE COOKIE *//
171
+ if (this.options.stepUpMfaGracePeriodSeconds) {
172
+ this.issueTempSkip2FAGraceJWT(opts);
173
+ }
98
174
  return { ok: true };
99
175
  }
100
176
  return { error: "Invalid passkey" };
@@ -543,7 +619,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
543
619
  server.endpoint({
544
620
  method: "GET",
545
621
  path: "/plugin/twofa/skip-allow-modal",
546
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
622
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, headers, cookies }) {
547
623
  if (this.options.usersFilterToApply) {
548
624
  const res = this.options.usersFilterToApply(adminUser);
549
625
  if (res === false) {
@@ -556,6 +632,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
556
632
  return { skipAllowed: true };
557
633
  }
558
634
  }
635
+ if (this.options.stepUpMfaGracePeriodSeconds) {
636
+ const verificationResult = yield this.isTempSkip2FAGraceValid(headers, cookies, true);
637
+ if (verificationResult === true) {
638
+ return { skipAllowed: true };
639
+ }
640
+ }
559
641
  return { skipAllowed: false };
560
642
  }),
561
643
  });
@@ -582,14 +664,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
582
664
  method: 'POST',
583
665
  path: `/plugin/passkeys/registerPasskeyRequest`,
584
666
  noAuth: false,
585
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
667
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, extra }) {
586
668
  var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
587
669
  const mode = body === null || body === void 0 ? void 0 : body.mode;
588
670
  const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
589
671
  const verificationResult = yield this.verify(confirmationResult, {
590
672
  adminUser: adminUser,
591
673
  userPk: adminUser.pk,
592
- cookies: cookies
674
+ cookies: cookies,
675
+ response: response,
676
+ extra: Object.assign({}, extra)
593
677
  });
594
678
  if (!verificationResult || !('ok' in verificationResult)) {
595
679
  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,63 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
54
55
  return { skipAllowed: false };
55
56
  }
56
57
 
58
+ private generateHashForStepUpMfaGraceCookie(headers, cookies): string {
59
+ const ip = this.adminforth.auth.getClientIp(headers);
60
+ const userAgent = headers['user-agent'] || '';
61
+ const acceptLanguage = headers['accept-language'] || '';
62
+ const session_cookie = this.adminforth.auth.getCustomCookie({cookies: cookies, name: "jwt"});
63
+ if (!ip || !userAgent || !acceptLanguage || !session_cookie) {
64
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: missing required request headers to identify client ❗️❗️❗️");
65
+ return null;
66
+ } else {
67
+ const hmac = crypto.createHmac('sha256', process.env.ADMINFORTH_SECRET)
68
+ .update(`${acceptLanguage}_${userAgent}_${ip}_${session_cookie}`)
69
+ .digest('hex');
70
+ return hmac;
71
+ }
72
+ }
73
+
74
+ private issueTempSkip2FAGraceJWT(opts): void {
75
+ if (opts.response) {
76
+ if (opts.extra.headers) {
77
+ const hash = this.generateHashForStepUpMfaGraceCookie(opts.extra.headers, opts.cookies);
78
+ if (!hash) {
79
+ return;
80
+ }
81
+ const jwt = this.adminforth.auth.issueJWT({ hash: hash }, 'MfaGrace', `${this.options.stepUpMfaGracePeriodSeconds}s`);
82
+ //TODO: fix ts-ignore after releasing new version of adminforth with updated types
83
+ //@ts-ignore
84
+ this.adminforth.auth.setCustomCookie({response: opts.response, payload: {name: "TempSkip2FA_Modal_JWT", value: jwt, expirySeconds: this.options.stepUpMfaGracePeriodSeconds, httpOnly: true}});
85
+ }
86
+ } else {
87
+ console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: response object is missing. You probably called verify() method without response parameter ❗️❗️❗️");
88
+ }
89
+ }
90
+
91
+ private async isTempSkip2FAGraceValid(headers, cookies, checkIfJWTAboutToExpire: boolean = false): Promise<boolean> {
92
+ const hash = this.generateHashForStepUpMfaGraceCookie(headers, cookies);
93
+ if (!hash) {
94
+ return false;
95
+ }
96
+ const jwt = this.adminforth.auth.getCustomCookie({cookies: cookies, name: "TempSkip2FA_Modal_JWT"});
97
+ const jwtVerificationResult = await this.adminforth.auth.verify(jwt, 'MfaGrace', false)
98
+ if (!jwtVerificationResult) {
99
+ return false;
100
+ }
101
+ const jwtHash = jwtVerificationResult['hash'];
102
+ if (checkIfJWTAboutToExpire && (jwtVerificationResult["exp"] - ( Date.now() / 1000)) < 30 ) {
103
+ console.error("❗️❗️❗️ Cannot validate step-up MFA grace cookie: token is expired or about to expire ❗️❗️❗️");
104
+ return false;
105
+ }
106
+ if (hash === jwtHash) {
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+
57
112
  public async verify(
58
113
  confirmationResult: Record<string, any>,
59
- opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any }
114
+ opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any, response?: IAdminForthHttpResponse, extra?: HttpExtra }
60
115
  ): Promise<{ ok: true } | { error: string }> {
61
116
  if (!confirmationResult) return { error: "Confirmation result is required" };
62
117
  if (this.options.usersFilterToApply) {
@@ -71,6 +126,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
71
126
  return { ok: true };
72
127
  }
73
128
  }
129
+ if ( this.options.stepUpMfaGracePeriodSeconds && opts.extra?.headers && !confirmationResult.mode) {
130
+ const verificationResult = await this.isTempSkip2FAGraceValid(opts.extra.headers, opts.cookies);
131
+ if ( verificationResult === true ) {
132
+ return { ok: true };
133
+ }
134
+ }
135
+
74
136
  if (confirmationResult.mode === "totp") {
75
137
  const code = confirmationResult.result;
76
138
  const authRes = this.adminforth.config.resources
@@ -92,7 +154,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
92
154
  if (!secret) return { error: "2FA is not set up for this user" };
93
155
 
94
156
  const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
95
- return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
157
+
158
+ //* SET GRACE COOKIE *//
159
+ if ( verified ) {
160
+ if (this.options.stepUpMfaGracePeriodSeconds) {
161
+ this.issueTempSkip2FAGraceJWT(opts);
162
+ }
163
+ return { ok: true }
164
+ } else {
165
+ return { error: "Wrong or expired OTP code" }
166
+ };
96
167
  } else if (confirmationResult.mode === "passkey") {
97
168
  const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: opts.cookies, name: `passkeyLoginTemporaryJWT`});
98
169
  if (!passkeysCookies) {
@@ -106,6 +177,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
106
177
  const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies );
107
178
 
108
179
  if (verificationResult.ok && verificationResult.passkeyConfirmed) {
180
+
181
+ //* SET GRACE COOKIE *//
182
+ if (this.options.stepUpMfaGracePeriodSeconds) {
183
+ this.issueTempSkip2FAGraceJWT(opts);
184
+ }
109
185
  return { ok: true }
110
186
  }
111
187
  return { error: "Invalid passkey" }
@@ -601,7 +677,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
601
677
  server.endpoint({
602
678
  method: "GET",
603
679
  path: "/plugin/twofa/skip-allow-modal",
604
- handler: async ({ adminUser }) => {
680
+ handler: async ({ adminUser, headers, cookies }) => {
605
681
  if ( this.options.usersFilterToApply ) {
606
682
  const res = this.options.usersFilterToApply(adminUser);
607
683
  if ( res === false ) {
@@ -614,6 +690,14 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
614
690
  return { skipAllowed: true };
615
691
  }
616
692
  }
693
+
694
+ if ( this.options.stepUpMfaGracePeriodSeconds ) {
695
+ const verificationResult = await this.isTempSkip2FAGraceValid(headers, cookies, true);
696
+ if ( verificationResult === true ) {
697
+ return { skipAllowed: true };
698
+ }
699
+ }
700
+
617
701
  return { skipAllowed: false };
618
702
  },
619
703
  });
@@ -641,14 +725,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
641
725
  method: 'POST',
642
726
  path: `/plugin/passkeys/registerPasskeyRequest`,
643
727
  noAuth: false,
644
- handler: async ({ body, adminUser, response, cookies }) => {
728
+ handler: async ({ body, adminUser, response, cookies, extra }) => {
645
729
  const mode = body?.mode;
646
730
 
647
731
  const confirmationResult = body?.confirmationResult;
648
732
  const verificationResult = await this.verify(confirmationResult, {
649
733
  adminUser: adminUser,
650
734
  userPk: adminUser.pk,
651
- cookies: cookies
735
+ cookies: cookies,
736
+ response: response,
737
+ extra: { ...extra }
652
738
  });
653
739
  if ( !verificationResult || !('ok' in verificationResult) ) {
654
740
  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.21",
3
+ "version": "2.10.1",
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
  /**