@adminforth/two-factors-auth 4.4.9 → 4.6.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 (34) hide show
  1. package/build.log +2 -2
  2. package/custom/TwoFAModal.vue +30 -19
  3. package/custom/TwoFactorsPasskeysSettings.vue +2 -4
  4. package/custom/use2faApi.ts +1 -0
  5. package/dist/custom/TwoFAModal.vue +30 -19
  6. package/dist/custom/TwoFactorsPasskeysSettings.vue +2 -4
  7. package/dist/custom/use2faApi.ts +1 -0
  8. package/dist/endpoints/registerPasskeyEndpoints.js +24 -0
  9. package/dist/endpoints/registerTwoFaEndpoints.js +18 -0
  10. package/dist/handlers/createPasskeyHandlers.d.ts +3 -6
  11. package/dist/handlers/createPasskeyHandlers.js +11 -15
  12. package/dist/index.d.ts +11 -7
  13. package/dist/index.js +23 -5
  14. package/dist/services/mfaRequestContext.d.ts +13 -0
  15. package/dist/services/mfaRequestContext.js +50 -0
  16. package/dist/services/totpService.d.ts +9 -10
  17. package/dist/services/totpService.js +27 -5
  18. package/dist/services/verifyMfaConfirmation.d.ts +7 -7
  19. package/dist/services/verifyMfaConfirmation.js +23 -20
  20. package/dist/utils/errors.d.ts +13 -0
  21. package/dist/utils/errors.js +14 -0
  22. package/dist/utils/types.d.ts +8 -0
  23. package/endpoints/registerPasskeyEndpoints.ts +24 -0
  24. package/endpoints/registerTwoFaEndpoints.ts +18 -0
  25. package/handlers/createPasskeyHandlers.ts +30 -16
  26. package/index.ts +28 -7
  27. package/package.json +3 -3
  28. package/services/mfaRequestContext.ts +51 -0
  29. package/services/totpService.ts +30 -7
  30. package/services/verifyMfaConfirmation.ts +30 -24
  31. package/tests/mfaRequestContext.test.mjs +79 -0
  32. package/tests/totpService.test.mjs +38 -0
  33. package/utils/errors.ts +14 -0
  34. package/utils/types.ts +4 -0
package/build.log CHANGED
@@ -17,5 +17,5 @@ custom/tsconfig.json
17
17
  custom/use2faApi.ts
18
18
  custom/utils.js
19
19
 
20
- sent 311,882 bytes received 248 bytes 624,260.00 bytes/sec
21
- total size is 310,909 speedup is 1.00
20
+ sent 312,646 bytes received 248 bytes 625,788.00 bytes/sec
21
+ total size is 311,686 speedup is 1.00
@@ -37,6 +37,7 @@
37
37
  @on-complete="handleOnComplete"
38
38
  />
39
39
  </div>
40
+ <p v-if="codeError" class="af-2fa-code-error text-center text-xs text-red-600 dark:text-red-400">{{ codeError }}</p>
40
41
  <p v-if="doesUserHavePasskeys" class="af-2fa-totp-footer text-center text-xs text-gray-500 dark:text-gray-400">
41
42
  {{$t('Having trouble?')}}
42
43
  <button type="button" class="af-2fa-switch-to-passkey text-lightPrimary dark:text-white hover:underline cursor-pointer" @click="modalMode = 'passkey'">{{$t('Use passkey instead')}}</button>
@@ -141,7 +142,7 @@
141
142
  if (props.adminUser) {
142
143
  websocket.unsubscribeByPrefix(`/user2fa/`);
143
144
  const user2FaTopic = `/user2fa/${props.adminUser.pk}/${getAdminForthClientId()}`;
144
- websocket.subscribe(user2FaTopic, async (data: {sessionId: string}) => {
145
+ websocket.subscribe(user2FaTopic, async (data: {sessionId: string; actionDescription?: string}) => {
145
146
  if (!allowAddNewSessions) {
146
147
  alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'});
147
148
  return;
@@ -153,7 +154,7 @@
153
154
  }
154
155
  try {
155
156
  isAwaiting2FAResult.value = true;
156
- confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult();
157
+ confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult(data.actionDescription);
157
158
  } catch (error) {
158
159
  if (!isTwoFaCancel(error)) {
159
160
  console.error('[AdminForth 2FA] Error during 2FA confirmation', error);
@@ -271,7 +272,8 @@
271
272
  const isLoading = ref(false);
272
273
  const isFetchingPasskey = ref(false);
273
274
  const customDialogTitle = ref("");
274
-
275
+ const codeError = ref("");
276
+
275
277
  async function usePasskeyButtonClick() {
276
278
  isFetchingPasskey.value = true;
277
279
  let passkeyData;
@@ -322,32 +324,41 @@
322
324
 
323
325
  async function sendConfirmationResult(value: string) {
324
326
  if (!resolveFn) throw new Error(t('Modal is not initialized properly'));
325
- if (verifyFn) {
326
- try {
327
- const ok = await verifyFn(value);
328
- if (!ok) {
329
- rejectFn?.(new Error(t('Invalid code')));
330
- return;
331
- }
332
- } catch (err) {
333
- rejectFn?.(err);
334
- return;
335
- }
327
+ let checked: { error: string } | { result: TwoFaConfirmationResult };
328
+ try {
329
+ checked = await checkCode(value);
330
+ } catch (err) {
331
+ rejectFn?.(err);
332
+ return;
333
+ }
334
+ if ('error' in checked) {
335
+ codeError.value = checked.error;
336
+ bindValue.value = '';
337
+ confirmationResult.value?.clearInput();
338
+ return;
336
339
  }
337
340
 
338
341
  modelShow.value = false;
339
- const dataToReturn = {
340
- mode: "totp",
341
- result: value
342
- }
342
+ codeError.value = "";
343
343
  customDialogTitle.value = "";
344
344
  removeEventListenerForOTPInput();
345
- resolveFn(dataToReturn);
345
+ resolveFn(checked.result);
346
346
  }
347
347
 
348
348
 
349
+ async function checkCode(code: string): Promise<{ error: string } | { result: TwoFaConfirmationResult }> {
350
+ if (verifyFn) {
351
+ return (await verifyFn(code)) ? { result: { mode: 'totp', result: code } } : { error: t('Wrong or expired OTP code') };
352
+ }
353
+ const response = await callAdminForthApi({ method: 'POST', path: '/plugin/twofa/verify', body: { code } });
354
+ return response?.ok
355
+ ? { result: { mode: 'verifiedTotp', result: response.verifiedCodeToken } }
356
+ : { error: response?.error ?? t('Wrong or expired OTP code') };
357
+ }
358
+
349
359
  function onCancel() {
350
360
  modelShow.value = false;
361
+ codeError.value = "";
351
362
  bindValue.value = '';
352
363
  confirmationResult.value?.clearInput();
353
364
  removeEventListenerForOTPInput();
@@ -130,7 +130,7 @@
130
130
  import { Dialog, Tooltip, Spinner, Input } from '@/afcl';
131
131
  import { IconTrashBinSolid, IconPenSolid, IconPlusOutline, IconInfoCircleOutline } from '@iconify-prerendered/vue-flowbite';
132
132
  import { useCoreStore } from '@/stores/core';
133
- import { useTwoFactorsAuth } from './use2faApi';
133
+ import { useTwoFactorsAuth, type TwoFaConfirmationResult } from './use2faApi';
134
134
 
135
135
  const { t } = useI18n();
136
136
  const coreStore = useCoreStore();
@@ -215,9 +215,7 @@
215
215
  }
216
216
  }
217
217
 
218
- type ConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record<string, any> } | { mode?: undefined };
219
-
220
- async function requestPasskeyChallenge(confirmationResult: ConfirmationResult) {
218
+ async function requestPasskeyChallenge(confirmationResult: TwoFaConfirmationResult) {
221
219
  try {
222
220
  const response = await callAdminForthApi({
223
221
  path: '/plugin/passkeys/registrationOptions',
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia';
3
3
 
4
4
  export type TwoFaConfirmationResult =
5
5
  | { mode: 'totp'; result: string }
6
+ | { mode: 'verifiedTotp'; result: string }
6
7
  | { mode: 'passkey'; result: Record<string, any> }
7
8
  | { mode?: undefined };
8
9
 
@@ -37,6 +37,7 @@
37
37
  @on-complete="handleOnComplete"
38
38
  />
39
39
  </div>
40
+ <p v-if="codeError" class="af-2fa-code-error text-center text-xs text-red-600 dark:text-red-400">{{ codeError }}</p>
40
41
  <p v-if="doesUserHavePasskeys" class="af-2fa-totp-footer text-center text-xs text-gray-500 dark:text-gray-400">
41
42
  {{$t('Having trouble?')}}
42
43
  <button type="button" class="af-2fa-switch-to-passkey text-lightPrimary dark:text-white hover:underline cursor-pointer" @click="modalMode = 'passkey'">{{$t('Use passkey instead')}}</button>
@@ -141,7 +142,7 @@
141
142
  if (props.adminUser) {
142
143
  websocket.unsubscribeByPrefix(`/user2fa/`);
143
144
  const user2FaTopic = `/user2fa/${props.adminUser.pk}/${getAdminForthClientId()}`;
144
- websocket.subscribe(user2FaTopic, async (data: {sessionId: string}) => {
145
+ websocket.subscribe(user2FaTopic, async (data: {sessionId: string; actionDescription?: string}) => {
145
146
  if (!allowAddNewSessions) {
146
147
  alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'});
147
148
  return;
@@ -153,7 +154,7 @@
153
154
  }
154
155
  try {
155
156
  isAwaiting2FAResult.value = true;
156
- confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult();
157
+ confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult(data.actionDescription);
157
158
  } catch (error) {
158
159
  if (!isTwoFaCancel(error)) {
159
160
  console.error('[AdminForth 2FA] Error during 2FA confirmation', error);
@@ -271,7 +272,8 @@
271
272
  const isLoading = ref(false);
272
273
  const isFetchingPasskey = ref(false);
273
274
  const customDialogTitle = ref("");
274
-
275
+ const codeError = ref("");
276
+
275
277
  async function usePasskeyButtonClick() {
276
278
  isFetchingPasskey.value = true;
277
279
  let passkeyData;
@@ -322,32 +324,41 @@
322
324
 
323
325
  async function sendConfirmationResult(value: string) {
324
326
  if (!resolveFn) throw new Error(t('Modal is not initialized properly'));
325
- if (verifyFn) {
326
- try {
327
- const ok = await verifyFn(value);
328
- if (!ok) {
329
- rejectFn?.(new Error(t('Invalid code')));
330
- return;
331
- }
332
- } catch (err) {
333
- rejectFn?.(err);
334
- return;
335
- }
327
+ let checked: { error: string } | { result: TwoFaConfirmationResult };
328
+ try {
329
+ checked = await checkCode(value);
330
+ } catch (err) {
331
+ rejectFn?.(err);
332
+ return;
333
+ }
334
+ if ('error' in checked) {
335
+ codeError.value = checked.error;
336
+ bindValue.value = '';
337
+ confirmationResult.value?.clearInput();
338
+ return;
336
339
  }
337
340
 
338
341
  modelShow.value = false;
339
- const dataToReturn = {
340
- mode: "totp",
341
- result: value
342
- }
342
+ codeError.value = "";
343
343
  customDialogTitle.value = "";
344
344
  removeEventListenerForOTPInput();
345
- resolveFn(dataToReturn);
345
+ resolveFn(checked.result);
346
346
  }
347
347
 
348
348
 
349
+ async function checkCode(code: string): Promise<{ error: string } | { result: TwoFaConfirmationResult }> {
350
+ if (verifyFn) {
351
+ return (await verifyFn(code)) ? { result: { mode: 'totp', result: code } } : { error: t('Wrong or expired OTP code') };
352
+ }
353
+ const response = await callAdminForthApi({ method: 'POST', path: '/plugin/twofa/verify', body: { code } });
354
+ return response?.ok
355
+ ? { result: { mode: 'verifiedTotp', result: response.verifiedCodeToken } }
356
+ : { error: response?.error ?? t('Wrong or expired OTP code') };
357
+ }
358
+
349
359
  function onCancel() {
350
360
  modelShow.value = false;
361
+ codeError.value = "";
351
362
  bindValue.value = '';
352
363
  confirmationResult.value?.clearInput();
353
364
  removeEventListenerForOTPInput();
@@ -130,7 +130,7 @@
130
130
  import { Dialog, Tooltip, Spinner, Input } from '@/afcl';
131
131
  import { IconTrashBinSolid, IconPenSolid, IconPlusOutline, IconInfoCircleOutline } from '@iconify-prerendered/vue-flowbite';
132
132
  import { useCoreStore } from '@/stores/core';
133
- import { useTwoFactorsAuth } from './use2faApi';
133
+ import { useTwoFactorsAuth, type TwoFaConfirmationResult } from './use2faApi';
134
134
 
135
135
  const { t } = useI18n();
136
136
  const coreStore = useCoreStore();
@@ -215,9 +215,7 @@
215
215
  }
216
216
  }
217
217
 
218
- type ConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record<string, any> } | { mode?: undefined };
219
-
220
- async function requestPasskeyChallenge(confirmationResult: ConfirmationResult) {
218
+ async function requestPasskeyChallenge(confirmationResult: TwoFaConfirmationResult) {
221
219
  try {
222
220
  const response = await callAdminForthApi({
223
221
  path: '/plugin/passkeys/registrationOptions',
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia';
3
3
 
4
4
  export type TwoFaConfirmationResult =
5
5
  | { mode: 'totp'; result: string }
6
+ | { mode: 'verifiedTotp'; result: string }
6
7
  | { mode: 'passkey'; result: Record<string, any> }
7
8
  | { mode?: undefined };
8
9
 
@@ -12,6 +12,9 @@ export function registerPasskeyEndpoints(server, handlers) {
12
12
  server.endpoint({
13
13
  method: 'POST',
14
14
  path: `/plugin/passkeys/registrationOptions`,
15
+ agent: {
16
+ hiddenFromAgents: true,
17
+ },
15
18
  noAuth: false,
16
19
  request_schema: registrationOptionsBodySchema,
17
20
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
@@ -21,6 +24,9 @@ export function registerPasskeyEndpoints(server, handlers) {
21
24
  server.endpoint({
22
25
  method: 'POST',
23
26
  path: `/plugin/passkeys/finishRegistration`,
27
+ agent: {
28
+ hiddenFromAgents: true,
29
+ },
24
30
  noAuth: false,
25
31
  request_schema: finishRegistrationBodySchema,
26
32
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, cookies, response }) {
@@ -30,18 +36,27 @@ export function registerPasskeyEndpoints(server, handlers) {
30
36
  server.endpoint({
31
37
  method: 'POST',
32
38
  path: `/plugin/passkeys/loginOptions`,
39
+ agent: {
40
+ hiddenFromAgents: true,
41
+ },
33
42
  noAuth: true,
34
43
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ response, headers }) { return handlers.createLoginOptions({ response, headers }); }),
35
44
  });
36
45
  server.endpoint({
37
46
  method: 'GET',
38
47
  path: `/plugin/passkeys/getPasskeys`,
48
+ agent: {
49
+ hiddenFromAgents: true,
50
+ },
39
51
  noAuth: false,
40
52
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response }) { return handlers.getPasskeys({ adminUser, response }); }),
41
53
  });
42
54
  server.endpoint({
43
55
  method: 'DELETE',
44
56
  path: `/plugin/passkeys/deletePasskey`,
57
+ agent: {
58
+ hiddenFromAgents: true,
59
+ },
45
60
  noAuth: false,
46
61
  request_schema: deletePasskeyBodySchema,
47
62
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
@@ -51,6 +66,9 @@ export function registerPasskeyEndpoints(server, handlers) {
51
66
  server.endpoint({
52
67
  method: 'POST',
53
68
  path: `/plugin/passkeys/renamePasskey`,
69
+ agent: {
70
+ hiddenFromAgents: true,
71
+ },
54
72
  noAuth: false,
55
73
  request_schema: renamePasskeyBodySchema,
56
74
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
@@ -60,12 +78,18 @@ export function registerPasskeyEndpoints(server, handlers) {
60
78
  server.endpoint({
61
79
  method: 'POST',
62
80
  path: `/plugin/passkeys/checkIfUserHasPasskeys`,
81
+ agent: {
82
+ hiddenFromAgents: true,
83
+ },
63
84
  noAuth: true,
64
85
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies, response }) { return handlers.checkIfUserHasPasskeys({ cookies, response }); }),
65
86
  });
66
87
  server.endpoint({
67
88
  method: 'POST',
68
89
  path: `/plugin/passkeys/resolveVerifyAuto`,
90
+ agent: {
91
+ hiddenFromAgents: true,
92
+ },
69
93
  noAuth: false,
70
94
  request_schema: resolveVerifyAutoBodySchema,
71
95
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
@@ -12,12 +12,18 @@ export function registerTwoFaEndpoints(server, handlers) {
12
12
  server.endpoint({
13
13
  method: 'POST',
14
14
  path: `/plugin/twofa/initSetup`,
15
+ agent: {
16
+ hiddenFromAgents: true,
17
+ },
15
18
  noAuth: true,
16
19
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies, response }) { return handlers.initSetup({ cookies, response }); }),
17
20
  });
18
21
  server.endpoint({
19
22
  method: 'POST',
20
23
  path: `/plugin/twofa/confirmLogin`,
24
+ agent: {
25
+ hiddenFromAgents: true,
26
+ },
21
27
  noAuth: true,
22
28
  request_schema: confirmLoginBodySchema,
23
29
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, cookies, headers, requestUrl, query }) {
@@ -27,6 +33,9 @@ export function registerTwoFaEndpoints(server, handlers) {
27
33
  server.endpoint({
28
34
  method: 'POST',
29
35
  path: `/plugin/twofa/confirmLoginWithPasskey`,
36
+ agent: {
37
+ hiddenFromAgents: true,
38
+ },
30
39
  noAuth: true,
31
40
  request_schema: confirmLoginWithPasskeyBodySchema,
32
41
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, cookies, headers, requestUrl, query }) {
@@ -36,17 +45,26 @@ export function registerTwoFaEndpoints(server, handlers) {
36
45
  server.endpoint({
37
46
  method: "GET",
38
47
  path: "/plugin/twofa/skip-allow",
48
+ agent: {
49
+ hiddenFromAgents: true,
50
+ },
39
51
  noAuth: true,
40
52
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies, response }) { return handlers.skipAllow({ cookies, response }); }),
41
53
  });
42
54
  server.endpoint({
43
55
  method: "GET",
44
56
  path: "/plugin/twofa/skip-allow-modal",
57
+ agent: {
58
+ hiddenFromAgents: true,
59
+ },
45
60
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, headers, cookies, response }) { return handlers.skipAllowModal({ adminUser, headers, cookies, response }); }),
46
61
  });
47
62
  server.endpoint({
48
63
  method: 'POST',
49
64
  path: `/plugin/twofa/verify`,
65
+ agent: {
66
+ hiddenFromAgents: true,
67
+ },
50
68
  noAuth: false,
51
69
  request_schema: verifyTotpBodySchema,
52
70
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, body, response }) {
@@ -1,3 +1,4 @@
1
+ import { TwoFactorErrorCode } from "../utils/errors.js";
1
2
  export declare function createPasskeyHandlers(ctx: any): {
2
3
  createRegistrationOptions: ({ body, adminUser, response, cookies, headers }: {
3
4
  body: any;
@@ -41,12 +42,8 @@ export declare function createPasskeyHandlers(ctx: any): {
41
42
  headers: any;
42
43
  }) => Promise<{
43
44
  ok: boolean;
44
- code: string;
45
- error: any;
46
- } | {
47
- ok: boolean;
48
- error: any;
49
- code?: undefined;
45
+ code: TwoFactorErrorCode;
46
+ error: string;
50
47
  } | {
51
48
  ok: boolean;
52
49
  }>;
@@ -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 { HttpStatus, errorMessage, respondWithStatus } from "../utils/errors.js";
10
+ import { HttpStatus, TwoFactorErrorCode, errorMessage, respondWithStatus } from "../utils/errors.js";
11
11
  export function createPasskeyHandlers(ctx) {
12
12
  return {
13
13
  createRegistrationOptions: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
@@ -73,12 +73,11 @@ export function createPasskeyHandlers(ctx) {
73
73
  return checkResult;
74
74
  }),
75
75
  resolveVerifyAuto: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
76
- var _b;
77
76
  const sessionsIds = body === null || body === void 0 ? void 0 : body.sessionsIds;
78
77
  const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
79
78
  const idsToResolve = Array.isArray(sessionsIds) ? sessionsIds : [];
80
79
  const resolveAllIdsAsFailed = (message, code) => {
81
- const payload = code ? { ok: false, code, error: message } : { ok: false, error: message };
80
+ const payload = { ok: false, code, error: message };
82
81
  for (const id of idsToResolve) {
83
82
  ctx.autoVerify.resolveResponse(id, payload);
84
83
  }
@@ -86,15 +85,15 @@ export function createPasskeyHandlers(ctx) {
86
85
  };
87
86
  try {
88
87
  if (!idsToResolve.length || !confirmationResult) {
89
- return respondWithStatus(resolveAllIdsAsFailed('Confirmation window was closed', 'VERIFICATION_CANCELLED'), response, HttpStatus.BAD_REQUEST);
88
+ return respondWithStatus(resolveAllIdsAsFailed('Confirmation window was closed', TwoFactorErrorCode.CANCELLED), response, HttpStatus.BAD_REQUEST);
90
89
  }
91
90
  for (const id of idsToResolve) {
92
91
  const validationResult = yield ctx.adminforth.auth.verify(id, 'auto2FA', false);
93
92
  if (!validationResult) {
94
- return respondWithStatus(resolveAllIdsAsFailed('Invalid session ID or confirmation result'), response, HttpStatus.FORBIDDEN);
93
+ return respondWithStatus(resolveAllIdsAsFailed('Invalid session ID or confirmation result', TwoFactorErrorCode.INVALID_SESSION), response, HttpStatus.FORBIDDEN);
95
94
  }
96
95
  if (validationResult.adminUserPk !== adminUser.pk) {
97
- return respondWithStatus(resolveAllIdsAsFailed('Session does not belong to the authenticated user'), response, HttpStatus.FORBIDDEN);
96
+ return respondWithStatus(resolveAllIdsAsFailed('Session does not belong to the authenticated user', TwoFactorErrorCode.INVALID_SESSION), response, HttpStatus.FORBIDDEN);
98
97
  }
99
98
  }
100
99
  const verificationResult = yield ctx.verifyMfaConfirmation(confirmationResult, {
@@ -106,20 +105,17 @@ export function createPasskeyHandlers(ctx) {
106
105
  headers: headers,
107
106
  }
108
107
  });
109
- if (!verificationResult || !('ok' in verificationResult)) {
110
- return respondWithStatus(resolveAllIdsAsFailed((_b = verificationResult === null || verificationResult === void 0 ? void 0 : verificationResult.error) !== null && _b !== void 0 ? _b : 'Verification failed'), response, HttpStatus.FORBIDDEN);
108
+ if ('error' in verificationResult) {
109
+ return respondWithStatus(resolveAllIdsAsFailed(verificationResult.error, verificationResult.code), response, HttpStatus.FORBIDDEN);
111
110
  }
112
- if ('ok' in verificationResult && verificationResult.ok) {
113
- for (const id of idsToResolve) {
114
- ctx.autoVerify.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
115
- }
116
- return { ok: true };
111
+ for (const id of idsToResolve) {
112
+ ctx.autoVerify.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
117
113
  }
118
- return respondWithStatus(resolveAllIdsAsFailed('Verification failed'), response, HttpStatus.FORBIDDEN);
114
+ return { ok: true };
119
115
  }
120
116
  catch (error) {
121
117
  console.error('[AdminForth 2FA] Error resolving automatic 2FA verification', error);
122
- return respondWithStatus(resolveAllIdsAsFailed(errorMessage(error)), response, HttpStatus.INTERNAL_SERVER_ERROR);
118
+ return respondWithStatus(resolveAllIdsAsFailed(errorMessage(error), TwoFactorErrorCode.INTERNAL_ERROR), response, HttpStatus.INTERNAL_SERVER_ERROR);
123
119
  }
124
120
  }),
125
121
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdminForthPlugin, RateLimiter } from "adminforth";
1
+ import { AdminForthPlugin, HttpExtra, RateLimiter } from "adminforth";
2
2
  import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth } from "adminforth";
3
3
  import { PluginOptions } from "./types.js";
4
4
  import { PasskeyService } from "./services/passkeyService.js";
@@ -6,6 +6,10 @@ import { TotpService } from "./services/totpService.js";
6
6
  import { type MfaConfirmationResult, type VerifyOptions } from "./services/verifyMfaConfirmation.js";
7
7
  import { CookieService } from "./services/cookieService.js";
8
8
  import { UserRepository } from "./repositories/userRepository.js";
9
+ import { TwoFactorErrorCode } from "./utils/errors.js";
10
+ import type { VerificationResult } from "./utils/types.js";
11
+ export { TwoFactorErrorCode };
12
+ export type { VerificationResult };
9
13
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
10
14
  options: PluginOptions;
11
15
  adminforth: IAdminForth;
@@ -17,6 +21,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
17
21
  userRepository: UserRepository;
18
22
  passkeyLoginRateLimiters: RateLimiter[];
19
23
  private ctx;
24
+ private readonly mfaRequestContext;
20
25
  constructor(options: PluginOptions);
21
26
  instanceUniqueRepresentation(pluginOptions: any): string;
22
27
  checkIfSkipSetupAllowSkipVerify(adminUser: AdminUser): Promise<{
@@ -25,12 +30,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
25
30
  private pending;
26
31
  private waitForResponse;
27
32
  private resolveResponse;
28
- verifyAuto(adminUser: AdminUser): Promise<any>;
29
- verify(confirmationResult: MfaConfirmationResult, opts: VerifyOptions): Promise<{
30
- ok: true;
31
- } | {
32
- error: string;
33
- }>;
33
+ withVerificationContext<T>(confirmationResult: MfaConfirmationResult | undefined, extra: HttpExtra, callback: () => Promise<T>): Promise<(import("./utils/types.js").VerificationFailure & {
34
+ actionDescription?: string;
35
+ }) | T>;
36
+ verifyAuto(adminUser: AdminUser, actionDescription?: string): Promise<any>;
37
+ verify(confirmationResult: MfaConfirmationResult | undefined, opts: VerifyOptions): Promise<VerificationResult>;
34
38
  modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
35
39
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
36
40
  activate(resourceConfig: AdminForthResource, adminforth: IAdminForth): void;
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import * as AdminForthRuntime from "adminforth";
12
12
  import { PasskeyService } from "./services/passkeyService.js";
13
13
  import { TotpService } from "./services/totpService.js";
14
14
  import { verifyMfaConfirmation } from "./services/verifyMfaConfirmation.js";
15
+ import { MfaRequestContext } from './services/mfaRequestContext.js';
15
16
  import { CookieService } from "./services/cookieService.js";
16
17
  import { UserRepository } from "./repositories/userRepository.js";
17
18
  import { registerPasskeyEndpoints } from "./endpoints/registerPasskeyEndpoints.js";
@@ -19,12 +20,15 @@ import { registerTwoFaEndpoints } from "./endpoints/registerTwoFaEndpoints.js";
19
20
  import { createPasskeyHandlers } from "./handlers/createPasskeyHandlers.js";
20
21
  import { createTwoFaHandlers } from "./handlers/createTwoFaHandlers.js";
21
22
  import { parsePeriod } from "./utils/parsePeriod.js";
23
+ import { TwoFactorErrorCode } from "./utils/errors.js";
22
24
  import crypto from 'crypto';
25
+ export { TwoFactorErrorCode };
23
26
  const getRequestWebsocketClientId = AdminForthRuntime.getRequestWebsocketClientId;
24
27
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
25
28
  constructor(options) {
26
29
  super(options, import.meta.url);
27
30
  this.passkeyLoginRateLimiters = [];
31
+ this.mfaRequestContext = new MfaRequestContext();
28
32
  this.pending = new Map();
29
33
  this.options = options;
30
34
  this.shouldHaveSingleInstancePerWholeApp = () => true;
@@ -66,7 +70,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
66
70
  };
67
71
  timeout = setTimeout(() => {
68
72
  cleanup();
69
- resolve({ ok: false, error: 'Verification timed out' });
73
+ resolve({ ok: false, error: 'Verification timed out', code: TwoFactorErrorCode.TIMEOUT });
70
74
  }, timeoutMs);
71
75
  this.pending.set(id, (value) => {
72
76
  cleanup();
@@ -80,16 +84,30 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
80
84
  resolve(data);
81
85
  }
82
86
  }
83
- verifyAuto(adminUser) {
87
+ withVerificationContext(confirmationResult, extra, callback) {
88
+ return this.mfaRequestContext.run(confirmationResult, extra, callback);
89
+ }
90
+ verifyAuto(adminUser, actionDescription) {
84
91
  return __awaiter(this, void 0, void 0, function* () {
92
+ const contextualResult = yield this.mfaRequestContext.verify(adminUser, actionDescription, (confirmationResult, extra) => this.verify(confirmationResult, {
93
+ adminUser,
94
+ userPk: adminUser.pk,
95
+ extra,
96
+ }));
97
+ if (contextualResult)
98
+ return contextualResult;
85
99
  const websocketClientId = getRequestWebsocketClientId === null || getRequestWebsocketClientId === void 0 ? void 0 : getRequestWebsocketClientId();
86
100
  if (!websocketClientId) {
87
- return { ok: false, error: '2FA auto verification requires an active browser request context' };
101
+ return {
102
+ ok: false,
103
+ error: '2FA auto verification requires an active browser request context',
104
+ code: TwoFactorErrorCode.BROWSER_CONTEXT_REQUIRED,
105
+ };
88
106
  }
89
107
  const sessionId = crypto.randomUUID();
90
108
  const jwt = this.adminforth.auth.issueJWT({ sessionId, adminUserPk: adminUser.pk }, 'auto2FA', '5m');
91
109
  const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
92
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}`, { sessionId: jwt });
110
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}`, { sessionId: jwt, actionDescription });
93
111
  const result = yield resultPromise;
94
112
  this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}/resolve`, { sessionId: jwt });
95
113
  return result;
@@ -107,7 +125,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
107
125
  this.adminForthAuth = adminforth.auth;
108
126
  this.userRepository = new UserRepository(this.adminforth, this.options);
109
127
  this.cookieService = new CookieService(this.adminforth, this.options);
110
- this.totpService = new TotpService(this.options, this.userRepository);
128
+ this.totpService = new TotpService(this.options, this.userRepository, this.adminforth.auth);
111
129
  this.passkeyService = new PasskeyService(this.options, this.adminforth, this.userRepository, undefined, this.cookieService);
112
130
  this.passkeyLoginRateLimiters = adminforth.config.auth.rateLimit.map((rate) => new RateLimiter(rate));
113
131
  this.ctx = {
@@ -0,0 +1,13 @@
1
+ import type { AdminUser, HttpExtra } from 'adminforth';
2
+ import type { MfaConfirmationResult } from './verifyMfaConfirmation.js';
3
+ import type { VerificationFailure, VerificationResult } from '../utils/types.js';
4
+ type ContextualVerificationFailure = VerificationFailure & {
5
+ actionDescription?: string;
6
+ };
7
+ /** Keeps verification within one external request, including every CRUD hook it invokes. */
8
+ export declare class MfaRequestContext {
9
+ private readonly storage;
10
+ run<T>(confirmationResult: MfaConfirmationResult | undefined, extra: HttpExtra, callback: () => Promise<T>): Promise<T | ContextualVerificationFailure>;
11
+ verify(adminUser: AdminUser, actionDescription: string | undefined, verifier: (confirmationResult: MfaConfirmationResult | undefined, extra: HttpExtra) => Promise<VerificationResult>): Promise<VerificationResult | undefined>;
12
+ }
13
+ export {};