@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 +91 -7
- package/index.ts +91 -5
- package/package.json +1 -1
- package/types.ts +6 -0
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
/**
|