@adminforth/two-factors-auth 3.3.9 → 4.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.
Files changed (54) hide show
  1. package/build.log +2 -2
  2. package/custom/TwoFAModal.vue +4 -4
  3. package/custom/TwoFactorsConfirmation.vue +9 -10
  4. package/custom/TwoFactorsPasskeysSettings.vue +10 -10
  5. package/custom/utils.js +7 -8
  6. package/dist/custom/TwoFAModal.vue +4 -4
  7. package/dist/custom/TwoFactorsConfirmation.vue +9 -10
  8. package/dist/custom/TwoFactorsPasskeysSettings.vue +10 -10
  9. package/dist/custom/utils.js +7 -8
  10. package/dist/endpoints/registerPasskeyEndpoints.d.ts +2 -0
  11. package/dist/endpoints/registerPasskeyEndpoints.js +59 -0
  12. package/dist/endpoints/registerTwoFaEndpoints.d.ts +2 -0
  13. package/dist/endpoints/registerTwoFaEndpoints.js +46 -0
  14. package/dist/handlers/createPasskeyHandlers.d.ts +48 -0
  15. package/dist/handlers/createPasskeyHandlers.js +99 -0
  16. package/dist/handlers/createTwoFaHandlers.d.ts +56 -0
  17. package/dist/handlers/createTwoFaHandlers.js +167 -0
  18. package/dist/index.d.ts +37 -0
  19. package/dist/index.js +51 -858
  20. package/dist/repositories/passkeyRepository.d.ts +18 -0
  21. package/dist/repositories/passkeyRepository.js +89 -0
  22. package/dist/repositories/userRepository.d.ts +14 -0
  23. package/dist/repositories/userRepository.js +45 -0
  24. package/dist/services/cookieService.d.ts +52 -0
  25. package/dist/services/cookieService.js +144 -0
  26. package/dist/services/passkeyService.d.ts +84 -0
  27. package/dist/services/passkeyService.js +369 -0
  28. package/dist/services/totpService.d.ts +21 -0
  29. package/dist/services/totpService.js +43 -0
  30. package/dist/services/verifyMfaConfirmation.d.ts +30 -0
  31. package/dist/services/verifyMfaConfirmation.js +67 -0
  32. package/dist/types.d.ts +116 -0
  33. package/dist/utils/errors.d.ts +4 -0
  34. package/dist/utils/errors.js +9 -0
  35. package/dist/utils/parsePeriod.d.ts +1 -0
  36. package/dist/utils/parsePeriod.js +17 -0
  37. package/dist/utils/types.d.ts +13 -0
  38. package/dist/utils/types.js +1 -0
  39. package/endpoints/registerPasskeyEndpoints.ts +61 -0
  40. package/endpoints/registerTwoFaEndpoints.ts +45 -0
  41. package/handlers/createPasskeyHandlers.ts +103 -0
  42. package/handlers/createTwoFaHandlers.ts +172 -0
  43. package/index.ts +57 -886
  44. package/package.json +1 -1
  45. package/repositories/passkeyRepository.ts +78 -0
  46. package/repositories/userRepository.ts +42 -0
  47. package/services/cookieService.ts +165 -0
  48. package/services/passkeyService.ts +375 -0
  49. package/services/totpService.ts +37 -0
  50. package/services/verifyMfaConfirmation.ts +92 -0
  51. package/tsconfig.json +1 -1
  52. package/utils/errors.ts +13 -0
  53. package/utils/parsePeriod.ts +19 -0
  54. package/utils/types.ts +17 -0
@@ -0,0 +1,17 @@
1
+ const PERIOD_RE = /^(\d+)([dhms])$/;
2
+ export function parsePeriod(period) {
3
+ if (!period)
4
+ return 0;
5
+ const match = PERIOD_RE.exec(period.trim());
6
+ if (!match)
7
+ throw new Error(`Invalid suggestionPeriod format: ${period}`);
8
+ const value = parseInt(match[1], 10);
9
+ const unit = match[2];
10
+ switch (unit) {
11
+ case 'd': return value * 24 * 60 * 60 * 1000;
12
+ case 'h': return value * 60 * 60 * 1000;
13
+ case 'm': return value * 60 * 1000;
14
+ case 's': return value * 1000;
15
+ default: return value;
16
+ }
17
+ }
@@ -0,0 +1,13 @@
1
+ import type { HttpExtra } from "adminforth";
2
+ export type AnyRecord = Record<string, any>;
3
+ export type CookieList = HttpExtra["cookies"];
4
+ export type HttpHeaders = HttpExtra["headers"];
5
+ export type ErrorResponse = {
6
+ ok: false;
7
+ code?: string;
8
+ error: string;
9
+ };
10
+ export type OkResponse<T extends AnyRecord = AnyRecord> = {
11
+ ok: true;
12
+ } & T;
13
+ export type PluginResponse<T extends AnyRecord = AnyRecord> = OkResponse<T> | ErrorResponse;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import type { IHttpServer } from "adminforth";
2
+
3
+ export function registerPasskeyEndpoints(server: IHttpServer, handlers: any): void {
4
+ server.endpoint({
5
+ method: 'POST',
6
+ path: `/plugin/passkeys/registrationOptions`,
7
+ noAuth: false,
8
+ handler: async ({ body, adminUser, response, cookies, headers }) =>
9
+ handlers.createRegistrationOptions({ body, adminUser, response, cookies, headers }),
10
+ });
11
+
12
+ server.endpoint({
13
+ method: 'POST',
14
+ path: `/plugin/passkeys/finishRegistration`,
15
+ noAuth: false,
16
+ handler: async ({ body, adminUser, cookies }) => handlers.finishRegistration({ body, adminUser, cookies }),
17
+ });
18
+
19
+ server.endpoint({
20
+ method: 'POST',
21
+ path: `/plugin/passkeys/loginOptions`,
22
+ noAuth: true,
23
+ handler: async ({ response }) => handlers.createLoginOptions({ response }),
24
+ });
25
+
26
+ server.endpoint({
27
+ method: 'GET',
28
+ path: `/plugin/passkeys/getPasskeys`,
29
+ noAuth: false,
30
+ handler: async ({ adminUser }) => handlers.getPasskeys({ adminUser }),
31
+ });
32
+
33
+ server.endpoint({
34
+ method: 'DELETE',
35
+ path: `/plugin/passkeys/deletePasskey`,
36
+ noAuth: false,
37
+ handler: async ({ body, adminUser }) => handlers.deletePasskey({ body, adminUser }),
38
+ });
39
+
40
+ server.endpoint({
41
+ method: 'POST',
42
+ path: `/plugin/passkeys/renamePasskey`,
43
+ noAuth: false,
44
+ handler: async ({ body, adminUser }) => handlers.renamePasskey({ body, adminUser }),
45
+ });
46
+
47
+ server.endpoint({
48
+ method: 'POST',
49
+ path: `/plugin/passkeys/checkIfUserHasPasskeys`,
50
+ noAuth: true,
51
+ handler: async ({ cookies }) => handlers.checkIfUserHasPasskeys({ cookies }),
52
+ });
53
+
54
+ server.endpoint({
55
+ method: 'POST',
56
+ path: `/plugin/passkeys/resolveVerifyAuto`,
57
+ noAuth: false,
58
+ handler: async ({ body, adminUser, response, cookies, headers }) =>
59
+ handlers.resolveVerifyAuto({ body, adminUser, response, cookies, headers }),
60
+ });
61
+ }
@@ -0,0 +1,45 @@
1
+ import type { IHttpServer } from "adminforth";
2
+
3
+ export function registerTwoFaEndpoints(server: IHttpServer, handlers: any): void {
4
+ server.endpoint({
5
+ method: 'POST',
6
+ path: `/plugin/twofa/initSetup`,
7
+ noAuth: true,
8
+ handler: async ({ cookies }) => handlers.initSetup({ cookies }),
9
+ });
10
+
11
+ server.endpoint({
12
+ method: 'POST',
13
+ path: `/plugin/twofa/confirmLogin`,
14
+ noAuth: true,
15
+ handler: async ({ body, response, cookies }) => handlers.confirmLogin({ body, response, cookies }),
16
+ });
17
+
18
+ server.endpoint({
19
+ method: 'POST',
20
+ path: `/plugin/twofa/confirmLoginWithPasskey`,
21
+ noAuth: true,
22
+ handler: async ({ body, response, cookies, headers, requestUrl, query }) =>
23
+ handlers.confirmLoginWithPasskey({ body, response, cookies, headers, requestUrl, query }),
24
+ });
25
+
26
+ server.endpoint({
27
+ method: "GET",
28
+ path: "/plugin/twofa/skip-allow",
29
+ noAuth: true,
30
+ handler: async ({ cookies }) => handlers.skipAllow({ cookies }),
31
+ });
32
+
33
+ server.endpoint({
34
+ method: "GET",
35
+ path: "/plugin/twofa/skip-allow-modal",
36
+ handler: async ({ adminUser, headers, cookies }) => handlers.skipAllowModal({ adminUser, headers, cookies }),
37
+ });
38
+
39
+ server.endpoint({
40
+ method: 'POST',
41
+ path: `/plugin/twofa/verify`,
42
+ noAuth: false,
43
+ handler: async ({ adminUser, body }) => handlers.verifyTotp({ adminUser, body }),
44
+ });
45
+ }
@@ -0,0 +1,103 @@
1
+ import type { HttpExtra } from "adminforth";
2
+ import { errorMessage } from "../utils/errors.js";
3
+
4
+ export function createPasskeyHandlers(ctx: any) {
5
+ return {
6
+ createRegistrationOptions: async ({ body, adminUser, response, cookies, headers }) => {
7
+ const mode = body?.mode;
8
+
9
+ const confirmationResult = body?.confirmationResult;
10
+ const verificationResult = await ctx.verifyMfaConfirmation(confirmationResult, {
11
+ adminUser: adminUser,
12
+ userPk: adminUser.pk,
13
+ cookies: cookies,
14
+ response: response,
15
+ extra: {
16
+ headers,
17
+ } as HttpExtra
18
+ });
19
+ if (!verificationResult || !('ok' in verificationResult)) {
20
+ return { ok: false, error: 'error' in verificationResult ? verificationResult.error : 'Verification failed' };
21
+ }
22
+
23
+ return ctx.passkeyService.createRegistrationOptions(mode, adminUser, response);
24
+ },
25
+
26
+ finishRegistration: async ({ body, adminUser, cookies }) => {
27
+ return ctx.passkeyService.finishRegistration(body, adminUser, cookies);
28
+ },
29
+
30
+ createLoginOptions: async ({ response }) => {
31
+ return ctx.passkeyService.createLoginOptions(response);
32
+ },
33
+
34
+ getPasskeys: async ({ adminUser }) => {
35
+ return ctx.passkeyService.getPasskeys(adminUser);
36
+ },
37
+
38
+ deletePasskey: async ({ body, adminUser }) => {
39
+ return ctx.passkeyService.deletePasskey(body.passkeyId, adminUser);
40
+ },
41
+
42
+ renamePasskey: async ({ body, adminUser }) => {
43
+ return ctx.passkeyService.renamePasskey(body.passkeyId, body.newName, adminUser);
44
+ },
45
+
46
+ checkIfUserHasPasskeys: async ({ cookies }) => {
47
+ return ctx.passkeyService.checkIfUserHasPasskeys(cookies);
48
+ },
49
+
50
+ resolveVerifyAuto: async ({ body, adminUser, response, cookies, headers }) => {
51
+ const sessionsIds = body?.sessionsIds;
52
+ const confirmationResult = body?.confirmationResult;
53
+ const idsToResolve = Array.isArray(sessionsIds) ? sessionsIds : [];
54
+
55
+ const resolveAllIdsAsFailed = (message, code?: string) => {
56
+ const payload = code ? { ok: false, code, error: message } : { ok: false, error: message };
57
+ for (const id of idsToResolve) {
58
+ ctx.autoVerify.resolveResponse(id, payload);
59
+ }
60
+ return payload;
61
+ }
62
+
63
+ try {
64
+ if (!idsToResolve.length || !confirmationResult) {
65
+ return(resolveAllIdsAsFailed('Confirmation window was closed', 'VERIFICATION_CANCELLED'));
66
+ }
67
+
68
+ for (const id of idsToResolve) {
69
+ const validationResult = await ctx.adminforth.auth.verify(id, 'auto2FA', false);
70
+ if (!validationResult) {
71
+ return(resolveAllIdsAsFailed('Invalid session ID or confirmation result'));
72
+ }
73
+ if (validationResult.adminUserPk !== adminUser.pk) {
74
+ return(resolveAllIdsAsFailed('Session does not belong to the authenticated user'));
75
+ }
76
+ }
77
+
78
+ const verificationResult = await ctx.verifyMfaConfirmation(confirmationResult, {
79
+ adminUser: adminUser,
80
+ userPk: adminUser.pk,
81
+ cookies: cookies,
82
+ response: response,
83
+ extra: {
84
+ headers: headers,
85
+ } as HttpExtra
86
+ });
87
+ if ( !verificationResult || !('ok' in verificationResult) ) {
88
+ return(resolveAllIdsAsFailed(verificationResult?.error ?? 'Verification failed'));
89
+ }
90
+ if ('ok' in verificationResult && verificationResult.ok){
91
+ for (const id of idsToResolve) {
92
+ ctx.autoVerify.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
93
+ }
94
+ return { ok: true };
95
+ }
96
+ return(resolveAllIdsAsFailed('Verification failed'));
97
+ } catch (error) {
98
+ console.error('[AdminForth 2FA] Error resolving automatic 2FA verification', error);
99
+ return(resolveAllIdsAsFailed(errorMessage(error)));
100
+ }
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,172 @@
1
+ export function createTwoFaHandlers(ctx: any) {
2
+ return {
3
+ initSetup: async ({ cookies }) => {
4
+ const toReturn = {totpJWT:null,status:'ok',}
5
+ const totpTemporaryJWT = ctx.cookieService.getTotpTemporary(cookies);
6
+ if (totpTemporaryJWT){
7
+ toReturn.totpJWT = totpTemporaryJWT
8
+ }
9
+ return toReturn
10
+ },
11
+
12
+ confirmLogin: async ({ body, response, cookies }) => {
13
+ const totpTemporaryJWT = ctx.cookieService.getTotpTemporary(cookies);
14
+ if (!totpTemporaryJWT) {
15
+ return { error: 'Login session expired. Please log in again.' }
16
+ }
17
+ const decoded = await ctx.cookieService.verifyTotpTemporary(cookies);
18
+ if (!decoded) {
19
+ return { error: 'Login session expired. Please log in again.' }
20
+ }
21
+
22
+ if (decoded.newSecret) {
23
+ const verified = body.skip && decoded.userCanSkipSetup ? true : ctx.totpService.verifySecret(decoded.newSecret, body.code);
24
+ if (verified) {
25
+ if (!body.skip) {
26
+ await ctx.totpService.saveSecret(decoded.pk, decoded.newSecret);
27
+ }
28
+ ctx.cookieService.removeTotpTemporary(response)
29
+ ctx.cookieService.setAuthCookie({expireInDuration: decoded.sessionDuration, response, username:decoded.userName, pk:decoded.pk})
30
+ return { status: 'ok', allowedLogin: true }
31
+ } else {
32
+ return {error: 'Wrong or expired OTP code'}
33
+ }
34
+ }
35
+
36
+ let verified = false;
37
+ let verificationError = 'Wrong or expired OTP code';
38
+ if (body.usePasskey && ctx.options.passkeys) {
39
+ const cookiesValidationResult = await ctx.passkeyService.validateLoginChallengeCookie(cookies);
40
+ if (!cookiesValidationResult.ok) {
41
+ return { error: cookiesValidationResult.error };
42
+ }
43
+ const res = await ctx.passkeyService.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, cookiesValidationResult.decodedPasskeysCookies);
44
+ if (res.ok && res.passkeyConfirmed) {
45
+ verified = true;
46
+ } else {
47
+ verificationError = res.error ?? 'Invalid passkey';
48
+ }
49
+ } else {
50
+ const verificationResult = await ctx.totpService.verifyUserCode(decoded.pk, body.code);
51
+ verified = 'ok' in verificationResult && verificationResult.ok;
52
+ if (!verified) {
53
+ verificationError = verificationResult.error ?? verificationError;
54
+ }
55
+ }
56
+ if (verified) {
57
+ ctx.cookieService.removeTotpTemporary(response)
58
+ ctx.cookieService.setAuthCookie({expireInDuration: decoded.sessionDuration, response, username:decoded.userName, pk:decoded.pk})
59
+ return { status: 'ok', allowedLogin: true }
60
+ } else {
61
+ return {error: verificationError}
62
+ }
63
+ },
64
+
65
+ confirmLoginWithPasskey: async ({ body, response, cookies, headers, requestUrl, query }) => {
66
+ if (!ctx.options.passkeys || ctx.options.passkeys.allowLoginWithPasskeys === false) {
67
+ return { error: 'Login with passkeys is not allowed' };
68
+ }
69
+
70
+ const passkeyResponse = body.passkeyResponse;
71
+ if (!passkeyResponse) {
72
+ return { error: 'Passkey response is required' };
73
+ }
74
+
75
+ const passkeyLoginResult = await ctx.passkeyService.getLoginUserByPasskeyResponse(passkeyResponse, cookies);
76
+ if (!passkeyLoginResult.ok) {
77
+ return { error: passkeyLoginResult.error };
78
+ }
79
+ const user = passkeyLoginResult.user;
80
+ const username = user[ctx.adminforth.config.auth.usernameField];
81
+ const userPk = user[ctx.userRepository.getUserPkField()];
82
+
83
+ const adminUser = {
84
+ dbUser: user,
85
+ pk: userPk,
86
+ username,
87
+ };
88
+
89
+ const toReturn = { allowedLogin: true, error: '' };
90
+ const rememberMe = body?.rememberMe || false;
91
+ const rememberDaysAfterPasskeyLogin = ctx.options.passkeys.rememberDaysAfterPasskeyLogin
92
+ ? ctx.options.passkeys.rememberDaysAfterPasskeyLogin.toString().concat('d')
93
+ : null;
94
+ const expireInDuration = rememberMe
95
+ ? (rememberDaysAfterPasskeyLogin ?? ctx.adminforth.config.auth.rememberMeDuration ?? '30d')
96
+ : '1d';
97
+
98
+ await ctx.adminforth.restApi.processLoginCallbacks(
99
+ adminUser,
100
+ toReturn,
101
+ response,
102
+ {
103
+ headers,
104
+ cookies,
105
+ requestUrl,
106
+ query,
107
+ body: {},
108
+ response,
109
+ meta: {
110
+ loginAllowedByPasskeyDirectSignIn: true
111
+ },
112
+ },
113
+ expireInDuration,
114
+ );
115
+
116
+ if ( toReturn.allowedLogin === true ) {
117
+ ctx.cookieService.setAuthCookie({
118
+ response,
119
+ username,
120
+ pk: userPk,
121
+ expireInDuration,
122
+ });
123
+ }
124
+ return toReturn;
125
+ },
126
+
127
+ skipAllow: async ({ cookies }) => {
128
+ const decoded = await ctx.cookieService.verifyTotpTemporary(cookies);
129
+ if (!decoded) {
130
+ return { status: "error", message: "Invalid token" };
131
+ }
132
+ if (!decoded.newSecret) {
133
+ return { status: "ok", skipAllowed: false };
134
+ } else {
135
+ return {
136
+ status: "ok",
137
+ skipAllowed: decoded.userCanSkipSetup,
138
+ };
139
+ }
140
+ },
141
+
142
+ skipAllowModal: async ({ adminUser, headers, cookies }) => {
143
+ if ( ctx.options.usersFilterToApply ) {
144
+ const res = ctx.options.usersFilterToApply(adminUser);
145
+ if ( res === false ) {
146
+ return { skipAllowed: true };
147
+ }
148
+ }
149
+ if ( ctx.options.usersFilterToAllowSkipSetup ) {
150
+ const res = await ctx.checkIfSkipSetupAllowSkipVerify(adminUser);
151
+ if ( res.skipAllowed === true ) {
152
+ return { skipAllowed: true };
153
+ }
154
+ }
155
+
156
+ if ( ctx.options.stepUpMfaGracePeriodSeconds ) {
157
+ const verificationResult = await ctx.cookieService.isMfaGraceValid(headers, cookies, true);
158
+ if ( verificationResult === true ) {
159
+ return { skipAllowed: true };
160
+ }
161
+ }
162
+
163
+ return { skipAllowed: false };
164
+ },
165
+
166
+ verifyTotp: async ({ adminUser, body }) => {
167
+ if (!body?.code) return { error: 'Code is required' };
168
+
169
+ return ctx.totpService.verifyAdminUserCode(adminUser, body.code);
170
+ },
171
+ };
172
+ }