@adminforth/two-factors-auth 3.1.2 → 3.1.4

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/build.log CHANGED
@@ -16,5 +16,5 @@ custom/pnpm-lock.yaml
16
16
  custom/tsconfig.json
17
17
  custom/utils.js
18
18
 
19
- sent 282,665 bytes received 229 bytes 565,788.00 bytes/sec
20
- total size is 281,759 speedup is 1.00
19
+ sent 283,900 bytes received 229 bytes 568,258.00 bytes/sec
20
+ total size is 282,994 speedup is 1.00
@@ -4,6 +4,7 @@
4
4
  <div v-if="modalMode === 'totp'" class="af-two-factor-modal-totp flex flex-col items-center relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
5
5
  <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center">
6
6
  <p> {{ customDialogTitle }} </p>
7
+ <p class="text-red-500 font-semibold text-xl" v-if="sessionsIdsToResolve.length > 1"> You are confirming {{ sessionsIdsToResolve.length }} actions</p>
7
8
  <p>{{ $t('Please enter your authenticator code') }}</p>
8
9
  </div>
9
10
 
@@ -51,6 +52,7 @@
51
52
  <p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">{{$t('Passkey')}}</p>
52
53
  <div class="mb-2 max-w-[300px] text:gray-900 dark:text-gray-200">
53
54
  <p class="mb-2">{{customDialogTitle}} </p>
55
+ <p class="text-red-500 font-semibold text-xl text-center mb-12" v-if="sessionsIdsToResolve.length > 1"> You are confirming {{ sessionsIdsToResolve.length }} actions</p>
54
56
  <p>{{$t('Authenticate yourself using the button below')}}</p>
55
57
  </div>
56
58
  <Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
@@ -100,23 +102,47 @@
100
102
 
101
103
  const { alert } = useAdminforth();
102
104
 
103
- let currentSessionId: string | null = null;
105
+ const isAwaiting2FAResult = ref(false);
106
+ let allowAddNewSessions = true;
107
+ const ALLOW_NEW_SESSIONS_PERIOD = 1000;
108
+ const sessionsIdsToResolve = ref<string[]>([]);
109
+
110
+ watch(isAwaiting2FAResult, (awaiting) => {
111
+ if (awaiting) {
112
+ allowAddNewSessions = true;
113
+ setTimeout(() => {
114
+ if (isAwaiting2FAResult.value) {
115
+ allowAddNewSessions = false;
116
+ }
117
+ }, ALLOW_NEW_SESSIONS_PERIOD);
118
+ }
119
+ });
120
+
104
121
  watch( props, () => {
105
122
  if (props.adminUser) {
106
123
  websocket.unsubscribeByPrefix(`/user2fa/`);
107
124
  websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
108
- currentSessionId = data.sessionId;
125
+ if (!allowAddNewSessions) {
126
+ alert({message: 'Some process or user tries to add new actions to confirm. Action was blocked', variant: 'warning'});
127
+ return;
128
+ }
129
+ sessionsIdsToResolve.value.push(data.sessionId);
109
130
  let confirmationResult;
131
+ if (isAwaiting2FAResult.value) {
132
+ return;
133
+ }
110
134
  try {
135
+ isAwaiting2FAResult.value = true;
111
136
  confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
112
137
  } catch (error) {
113
138
  console.error('Error during 2FA confirmation:', error);
114
139
  }
140
+ isAwaiting2FAResult.value = false;
115
141
  try {
116
142
  const response = await callAdminForthApi({
117
143
  method: "POST",
118
144
  path: "/plugin/passkeys/resolveVerifyAuto",
119
- body: { confirmationResult, sessionId: data.sessionId }
145
+ body: { confirmationResult, sessionsIds: sessionsIdsToResolve.value }
120
146
  });
121
147
  if (!response.ok && response.error === 'No session ID or confirmation result'){
122
148
  alert({message: 'Verification session finished or cancelled.', variant: 'warning'});
@@ -125,15 +151,16 @@
125
151
  } else if (response.ok) {
126
152
  alert({message: 'Verification successful', variant: 'success'});
127
153
  }
154
+ sessionsIdsToResolve.value = [];
128
155
  } catch (error) {
129
156
  console.error('Error resolving automatic 2FA verification:', error);
130
157
  }
131
- currentSessionId = null;
158
+ allowAddNewSessions = true;
132
159
  });
133
160
  websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
134
- if (currentSessionId === data.sessionId && rejectFn && modelShow.value) {
161
+ if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
135
162
  onCancel();
136
- currentSessionId = null;
163
+ sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
137
164
  }
138
165
  });
139
166
  }
@@ -4,6 +4,7 @@
4
4
  <div v-if="modalMode === 'totp'" class="af-two-factor-modal-totp flex flex-col items-center relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
5
5
  <div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center">
6
6
  <p> {{ customDialogTitle }} </p>
7
+ <p class="text-red-500 font-semibold text-xl" v-if="sessionsIdsToResolve.length > 1"> You are confirming {{ sessionsIdsToResolve.length }} actions</p>
7
8
  <p>{{ $t('Please enter your authenticator code') }}</p>
8
9
  </div>
9
10
 
@@ -51,6 +52,7 @@
51
52
  <p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">{{$t('Passkey')}}</p>
52
53
  <div class="mb-2 max-w-[300px] text:gray-900 dark:text-gray-200">
53
54
  <p class="mb-2">{{customDialogTitle}} </p>
55
+ <p class="text-red-500 font-semibold text-xl text-center mb-12" v-if="sessionsIdsToResolve.length > 1"> You are confirming {{ sessionsIdsToResolve.length }} actions</p>
54
56
  <p>{{$t('Authenticate yourself using the button below')}}</p>
55
57
  </div>
56
58
  <Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
@@ -100,23 +102,47 @@
100
102
 
101
103
  const { alert } = useAdminforth();
102
104
 
103
- let currentSessionId: string | null = null;
105
+ const isAwaiting2FAResult = ref(false);
106
+ let allowAddNewSessions = true;
107
+ const ALLOW_NEW_SESSIONS_PERIOD = 1000;
108
+ const sessionsIdsToResolve = ref<string[]>([]);
109
+
110
+ watch(isAwaiting2FAResult, (awaiting) => {
111
+ if (awaiting) {
112
+ allowAddNewSessions = true;
113
+ setTimeout(() => {
114
+ if (isAwaiting2FAResult.value) {
115
+ allowAddNewSessions = false;
116
+ }
117
+ }, ALLOW_NEW_SESSIONS_PERIOD);
118
+ }
119
+ });
120
+
104
121
  watch( props, () => {
105
122
  if (props.adminUser) {
106
123
  websocket.unsubscribeByPrefix(`/user2fa/`);
107
124
  websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
108
- currentSessionId = data.sessionId;
125
+ if (!allowAddNewSessions) {
126
+ alert({message: 'Some process or user tries to add new actions to confirm. Action was blocked', variant: 'warning'});
127
+ return;
128
+ }
129
+ sessionsIdsToResolve.value.push(data.sessionId);
109
130
  let confirmationResult;
131
+ if (isAwaiting2FAResult.value) {
132
+ return;
133
+ }
110
134
  try {
135
+ isAwaiting2FAResult.value = true;
111
136
  confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
112
137
  } catch (error) {
113
138
  console.error('Error during 2FA confirmation:', error);
114
139
  }
140
+ isAwaiting2FAResult.value = false;
115
141
  try {
116
142
  const response = await callAdminForthApi({
117
143
  method: "POST",
118
144
  path: "/plugin/passkeys/resolveVerifyAuto",
119
- body: { confirmationResult, sessionId: data.sessionId }
145
+ body: { confirmationResult, sessionsIds: sessionsIdsToResolve.value }
120
146
  });
121
147
  if (!response.ok && response.error === 'No session ID or confirmation result'){
122
148
  alert({message: 'Verification session finished or cancelled.', variant: 'warning'});
@@ -125,15 +151,16 @@
125
151
  } else if (response.ok) {
126
152
  alert({message: 'Verification successful', variant: 'success'});
127
153
  }
154
+ sessionsIdsToResolve.value = [];
128
155
  } catch (error) {
129
156
  console.error('Error resolving automatic 2FA verification:', error);
130
157
  }
131
- currentSessionId = null;
158
+ allowAddNewSessions = true;
132
159
  });
133
160
  websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
134
- if (currentSessionId === data.sessionId && rejectFn && modelShow.value) {
161
+ if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
135
162
  onCancel();
136
- currentSessionId = null;
163
+ sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
137
164
  }
138
165
  });
139
166
  }
package/dist/index.js CHANGED
@@ -149,9 +149,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
149
149
  verifyAuto(adminUser) {
150
150
  return __awaiter(this, void 0, void 0, function* () {
151
151
  const sessionId = crypto.randomUUID();
152
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId });
153
- const result = yield this.waitForResponse(sessionId);
154
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId });
152
+ const jwt = this.adminforth.auth.issueJWT({ sessionId, adminUserPk: adminUser.pk }, 'auto2FA', '5m');
153
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
154
+ const result = yield this.waitForResponse(jwt);
155
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
155
156
  return result;
156
157
  });
157
158
  }
@@ -1019,11 +1020,26 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
1019
1020
  path: `/plugin/passkeys/resolveVerifyAuto`,
1020
1021
  noAuth: false,
1021
1022
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
1022
- const sessionId = body === null || body === void 0 ? void 0 : body.sessionId;
1023
+ const sessionsIds = body === null || body === void 0 ? void 0 : body.sessionsIds;
1023
1024
  const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
1024
- if (!sessionId || !confirmationResult) {
1025
- this.resolveResponse(sessionId, { ok: false, error: 'No session ID or confirmation result' });
1026
- return { ok: false, error: 'No session ID or confirmation result' };
1025
+ const idsToResolve = sessionsIds;
1026
+ const resolveAllIdsAsFailed = (message) => {
1027
+ for (const id of idsToResolve) {
1028
+ this.resolveResponse(id, { ok: false, error: message });
1029
+ }
1030
+ return { ok: false, error: message };
1031
+ };
1032
+ if (!(sessionsIds) || !confirmationResult) {
1033
+ resolveAllIdsAsFailed('Confirmation window was closed or did not return required data');
1034
+ }
1035
+ for (const id of idsToResolve) {
1036
+ const validationResult = yield this.adminforth.auth.verify(id, 'auto2FA', false);
1037
+ if (!validationResult) {
1038
+ resolveAllIdsAsFailed('Invalid session ID or confirmation result');
1039
+ }
1040
+ if (validationResult.adminUserPk !== adminUser.pk) {
1041
+ resolveAllIdsAsFailed('Session does not belong to the authenticated user');
1042
+ }
1027
1043
  }
1028
1044
  const verificationResult = yield this.verify(confirmationResult, {
1029
1045
  adminUser: adminUser,
@@ -1035,10 +1051,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
1035
1051
  }
1036
1052
  });
1037
1053
  if (!verificationResult || !('ok' in verificationResult)) {
1038
- this.resolveResponse(sessionId, { ok: false, error: 'Verification failed' });
1039
- return { ok: false, error: 'Verification failed' };
1054
+ resolveAllIdsAsFailed('Verification failed');
1055
+ }
1056
+ for (const id of idsToResolve) {
1057
+ this.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
1040
1058
  }
1041
- this.resolveResponse(sessionId, { ok: true, passkeyConfirmed: verificationResult });
1042
1059
  return { ok: true };
1043
1060
  })
1044
1061
  });
package/index.ts CHANGED
@@ -159,9 +159,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
159
159
 
160
160
  public async verifyAuto(adminUser: AdminUser) {
161
161
  const sessionId = crypto.randomUUID();
162
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId });
163
- const result = await this.waitForResponse(sessionId);
164
- this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId });
162
+ const jwt = this.adminforth.auth.issueJWT({sessionId, adminUserPk: adminUser.pk}, 'auto2FA', '5m');
163
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
164
+ const result = await this.waitForResponse(jwt);
165
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
165
166
  return result;
166
167
  }
167
168
 
@@ -1080,12 +1081,31 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
1080
1081
  path: `/plugin/passkeys/resolveVerifyAuto`,
1081
1082
  noAuth: false,
1082
1083
  handler: async ({ body, adminUser, response, cookies, headers }) => {
1083
- const sessionId = body?.sessionId;
1084
+ const sessionsIds = body?.sessionsIds;
1084
1085
  const confirmationResult = body?.confirmationResult;
1085
- if (!sessionId || !confirmationResult) {
1086
- this.resolveResponse(sessionId, { ok: false, error: 'No session ID or confirmation result' });
1087
- return { ok: false, error: 'No session ID or confirmation result' };
1086
+ const idsToResolve = sessionsIds;
1087
+
1088
+ const resolveAllIdsAsFailed = (message) => {
1089
+ for (const id of idsToResolve) {
1090
+ this.resolveResponse(id, { ok: false, error: message });
1091
+ }
1092
+ return { ok: false, error: message };
1093
+ }
1094
+
1095
+ if (!(sessionsIds) || !confirmationResult) {
1096
+ resolveAllIdsAsFailed('Confirmation window was closed or did not return required data');
1097
+ }
1098
+
1099
+ for (const id of idsToResolve) {
1100
+ const validationResult = await this.adminforth.auth.verify(id, 'auto2FA', false);
1101
+ if (!validationResult) {
1102
+ resolveAllIdsAsFailed('Invalid session ID or confirmation result');
1103
+ }
1104
+ if (validationResult.adminUserPk !== adminUser.pk) {
1105
+ resolveAllIdsAsFailed('Session does not belong to the authenticated user');
1106
+ }
1088
1107
  }
1108
+
1089
1109
  const verificationResult = await this.verify(confirmationResult, {
1090
1110
  adminUser: adminUser,
1091
1111
  userPk: adminUser.pk,
@@ -1096,10 +1116,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
1096
1116
  } as HttpExtra
1097
1117
  });
1098
1118
  if ( !verificationResult || !('ok' in verificationResult) ) {
1099
- this.resolveResponse(sessionId, { ok: false, error: 'Verification failed' });
1100
- return { ok: false, error: 'Verification failed' };
1119
+ resolveAllIdsAsFailed('Verification failed');
1120
+ }
1121
+
1122
+ for (const id of idsToResolve) {
1123
+ this.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
1101
1124
  }
1102
- this.resolveResponse(sessionId, { ok: true, passkeyConfirmed: verificationResult });
1103
1125
  return { ok: true };
1104
1126
  }
1105
1127
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",