@adminforth/two-factors-auth 4.0.1 → 4.1.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.
- package/build.log +3 -2
- package/custom/TwoFAModal.vue +52 -53
- package/custom/TwoFactorsPasskeysSettings.vue +7 -2
- package/custom/use2faApi.ts +52 -0
- package/dist/custom/TwoFAModal.vue +52 -53
- package/dist/custom/TwoFactorsPasskeysSettings.vue +7 -2
- package/dist/custom/use2faApi.ts +52 -0
- package/dist/index.js +8 -2
- package/index.ts +11 -2
- package/package.json +1 -1
- package/pnpm-workspace.yaml +4 -0
package/build.log
CHANGED
|
@@ -14,7 +14,8 @@ custom/package-lock.json
|
|
|
14
14
|
custom/package.json
|
|
15
15
|
custom/pnpm-lock.yaml
|
|
16
16
|
custom/tsconfig.json
|
|
17
|
+
custom/use2faApi.ts
|
|
17
18
|
custom/utils.js
|
|
18
19
|
|
|
19
|
-
sent
|
|
20
|
-
total size is
|
|
20
|
+
sent 288,076 bytes received 248 bytes 576,648.00 bytes/sec
|
|
21
|
+
total size is 287,093 speedup is 1.00
|
package/custom/TwoFAModal.vue
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
<script setup lang="ts">
|
|
102
102
|
|
|
103
103
|
import VOtpInput from 'vue3-otp-input';
|
|
104
|
-
import { ref, nextTick, watch
|
|
104
|
+
import { ref, nextTick, watch } from 'vue';
|
|
105
105
|
import { useUserStore } from '@/stores/user';
|
|
106
106
|
import { useI18n } from 'vue-i18n';
|
|
107
107
|
import { callAdminForthApi } from '@/utils';
|
|
@@ -110,26 +110,16 @@
|
|
|
110
110
|
import { getPasskey } from './utils.js'
|
|
111
111
|
import { useAdminforth } from '@/adminforth';
|
|
112
112
|
import websocket from '@/websocket';
|
|
113
|
+
import { getAdminForthClientId } from '@/utils/clientId';
|
|
113
114
|
import type { AdminUser } from '@/types/Common';
|
|
114
|
-
|
|
115
|
-
type TwoFaConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record<string, any> };
|
|
116
|
-
|
|
117
|
-
declare global {
|
|
118
|
-
interface Window {
|
|
119
|
-
adminforthTwoFaModal: {
|
|
120
|
-
get2FaConfirmationResult: (
|
|
121
|
-
title?: string,
|
|
122
|
-
verifyingCallback?: (confirmationResult: string) => Promise<boolean>
|
|
123
|
-
) => Promise<TwoFaConfirmationResult>;
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
115
|
+
import { useTwoFactorsAuthApi, type TwoFaConfirmationResult } from './use2faApi.js';
|
|
127
116
|
const props = defineProps<{
|
|
128
117
|
autoFinishLogin?: boolean
|
|
129
118
|
adminUser?: AdminUser
|
|
130
119
|
}>();
|
|
131
120
|
|
|
132
121
|
const { alert } = useAdminforth();
|
|
122
|
+
const twoFactorsAuthApi = useTwoFactorsAuthApi();
|
|
133
123
|
|
|
134
124
|
const isAwaiting2FAResult = ref(false);
|
|
135
125
|
let allowAddNewSessions = true;
|
|
@@ -150,7 +140,8 @@
|
|
|
150
140
|
watch( props, () => {
|
|
151
141
|
if (props.adminUser) {
|
|
152
142
|
websocket.unsubscribeByPrefix(`/user2fa/`);
|
|
153
|
-
|
|
143
|
+
const user2FaTopic = `/user2fa/${props.adminUser.pk}/${getAdminForthClientId()}`;
|
|
144
|
+
websocket.subscribe(user2FaTopic, async (data: {sessionId: string}) => {
|
|
154
145
|
if (!allowAddNewSessions) {
|
|
155
146
|
alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'});
|
|
156
147
|
return;
|
|
@@ -162,7 +153,7 @@
|
|
|
162
153
|
}
|
|
163
154
|
try {
|
|
164
155
|
isAwaiting2FAResult.value = true;
|
|
165
|
-
confirmationResult = await
|
|
156
|
+
confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult();
|
|
166
157
|
} catch (error) {
|
|
167
158
|
if (!isTwoFaCancel(error)) {
|
|
168
159
|
console.error('[AdminForth 2FA] Error during 2FA confirmation', error);
|
|
@@ -188,7 +179,7 @@
|
|
|
188
179
|
}
|
|
189
180
|
allowAddNewSessions = true;
|
|
190
181
|
});
|
|
191
|
-
websocket.subscribe(
|
|
182
|
+
websocket.subscribe(`${user2FaTopic}/resolve`, async (data: {sessionId: string}) => {
|
|
192
183
|
if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
|
|
193
184
|
onCancel();
|
|
194
185
|
sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
|
|
@@ -224,7 +215,6 @@
|
|
|
224
215
|
|
|
225
216
|
const modelShow = ref(false);
|
|
226
217
|
let resolveFn: ((confirmationResult: any) => void) | null = null;
|
|
227
|
-
let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
|
|
228
218
|
let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
|
|
229
219
|
let rejectFn: ((err?: any) => void) | null = null;
|
|
230
220
|
|
|
@@ -233,37 +223,42 @@
|
|
|
233
223
|
}
|
|
234
224
|
|
|
235
225
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
226
|
+
function get2FaConfirmationResult(
|
|
227
|
+
title?: string,
|
|
228
|
+
verifyingCallback?: (confirmationResult: string) => Promise<boolean> | boolean
|
|
229
|
+
): Promise<TwoFaConfirmationResult> {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
(async () => {
|
|
232
|
+
if (modelShow.value) {
|
|
233
|
+
throw new Error(t('Modal is already open'));
|
|
234
|
+
}
|
|
235
|
+
const skipAllowModal = await checkIfSkipAllowModal();
|
|
236
|
+
if (skipAllowModal) {
|
|
237
|
+
resolve({ mode: 'totp', result: '123456' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await checkIfUserHasPasskeys();
|
|
241
|
+
if (title) {
|
|
242
|
+
customDialogTitle.value = title;
|
|
243
|
+
}
|
|
244
|
+
resolveFn = resolve;
|
|
245
|
+
rejectFn = reject;
|
|
246
|
+
verifyFn = verifyingCallback ?? null;
|
|
247
|
+
modelShow.value = true;
|
|
248
|
+
if (modalMode.value === 'totp') {
|
|
249
|
+
await addEventListenerForOTPInput();
|
|
250
|
+
}
|
|
251
|
+
})().catch((error) => {
|
|
252
|
+
if (!isTwoFaCancel(error)) {
|
|
253
|
+
console.error('[AdminForth 2FA] Failed to open 2FA modal', error);
|
|
254
|
+
}
|
|
255
|
+
reject(error);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
twoFactorsAuthApi.setGet2FaConfirmationResultHandler(get2FaConfirmationResult);
|
|
261
|
+
twoFactorsAuthApi.initGlobalApi();
|
|
267
262
|
|
|
268
263
|
const { t } = useI18n();
|
|
269
264
|
const user = useUserStore();
|
|
@@ -274,15 +269,19 @@
|
|
|
274
269
|
const doesUserHavePasskeys = ref(false);
|
|
275
270
|
const modalMode = ref<"totp" | "passkey">("totp");
|
|
276
271
|
const isLoading = ref(false);
|
|
272
|
+
const isFetchingPasskey = ref(false);
|
|
277
273
|
const customDialogTitle = ref("");
|
|
278
274
|
|
|
279
275
|
async function usePasskeyButtonClick() {
|
|
276
|
+
isFetchingPasskey.value = true;
|
|
280
277
|
let passkeyData;
|
|
281
278
|
try {
|
|
282
279
|
passkeyData = await getPasskey();
|
|
283
280
|
} catch (error) {
|
|
284
281
|
onCancel();
|
|
285
282
|
return null;
|
|
283
|
+
} finally {
|
|
284
|
+
isFetchingPasskey.value = false;
|
|
286
285
|
}
|
|
287
286
|
modelShow.value = false;
|
|
288
287
|
const dataToReturn = {
|
|
@@ -291,7 +290,7 @@
|
|
|
291
290
|
}
|
|
292
291
|
customDialogTitle.value = "";
|
|
293
292
|
removeEventListenerForOTPInput();
|
|
294
|
-
resolveFn(dataToReturn);
|
|
293
|
+
resolveFn?.(dataToReturn);
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
function tagOtpInputs() {
|
|
@@ -352,7 +351,7 @@
|
|
|
352
351
|
bindValue.value = '';
|
|
353
352
|
confirmationResult.value?.clearInput();
|
|
354
353
|
removeEventListenerForOTPInput();
|
|
355
|
-
rejectFn("Cancel");
|
|
354
|
+
rejectFn?.("Cancel");
|
|
356
355
|
emit('rejected', new Error('cancelled'));
|
|
357
356
|
emit('closed');
|
|
358
357
|
}
|
|
@@ -438,10 +437,10 @@
|
|
|
438
437
|
((firstEmpty || inputs[0]) as HTMLInputElement).focus();
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
function handleGlobalFocusIn(event) {
|
|
440
|
+
function handleGlobalFocusIn(event: FocusEvent) {
|
|
442
441
|
const inputs = getOtpInputs();
|
|
443
442
|
if (!inputs.length) return;
|
|
444
|
-
const target = event.target;
|
|
443
|
+
const target = event.target as Element | null;
|
|
445
444
|
if (!target) return;
|
|
446
445
|
if (!inputs.includes(target)) {
|
|
447
446
|
requestAnimationFrame(() => {
|
|
@@ -133,11 +133,13 @@
|
|
|
133
133
|
import utc from 'dayjs/plugin/utc';
|
|
134
134
|
import timezone from 'dayjs/plugin/timezone';
|
|
135
135
|
import { useCoreStore } from '@/stores/core';
|
|
136
|
+
import { useTwoFactorsAuth } from './use2faApi';
|
|
136
137
|
|
|
137
138
|
const { t } = useI18n();
|
|
138
139
|
dayjs.extend(utc);
|
|
139
140
|
dayjs.extend(timezone);
|
|
140
141
|
const coreStore = useCoreStore();
|
|
142
|
+
const twoFactorsAuth = useTwoFactorsAuth();
|
|
141
143
|
|
|
142
144
|
interface Passkey {
|
|
143
145
|
id: string;
|
|
@@ -176,8 +178,9 @@
|
|
|
176
178
|
async function startAddPasskey() {
|
|
177
179
|
isFetchingPasskey.value = true;
|
|
178
180
|
try {
|
|
179
|
-
const confirmationResult = await
|
|
180
|
-
|
|
181
|
+
const confirmationResult = await twoFactorsAuth.get2FaConfirmationResult(
|
|
182
|
+
t('To add passkey first verify yourself')
|
|
183
|
+
);
|
|
181
184
|
|
|
182
185
|
if (!confirmationResult) return;
|
|
183
186
|
|
|
@@ -227,7 +230,9 @@
|
|
|
227
230
|
body: { mode: addPasskeyMode.value, confirmationResult },
|
|
228
231
|
});
|
|
229
232
|
if (!response.ok) return { error: response.error ?? t('Verification failed') };
|
|
233
|
+
console.log('Passkey challenge response:', response.data);
|
|
230
234
|
const options = PublicKeyCredential.parseCreationOptionsFromJSON(response.data);
|
|
235
|
+
console.log('Parsed PublicKeyCredentialCreationOptions:', options);
|
|
231
236
|
return { options, challengeId: response.challengeId };
|
|
232
237
|
} catch (error) {
|
|
233
238
|
console.error('Error requesting passkey challenge:', error);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { defineStore } from 'pinia';
|
|
3
|
+
|
|
4
|
+
export type TwoFaConfirmationResult =
|
|
5
|
+
| { mode: 'totp'; result: string }
|
|
6
|
+
| { mode: 'passkey'; result: Record<string, any> };
|
|
7
|
+
|
|
8
|
+
type VerifyFn = (confirmationResult: string) => Promise<boolean> | boolean;
|
|
9
|
+
type Get2FaConfirmationResultHandler = (
|
|
10
|
+
title?: string,
|
|
11
|
+
verifyingCallback?: VerifyFn
|
|
12
|
+
) => Promise<TwoFaConfirmationResult>;
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
adminforthTwoFaModal: {
|
|
17
|
+
get2FaConfirmationResult: Get2FaConfirmationResultHandler;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const useTwoFactorsAuthApi = defineStore('twoFactorsAuthApi', () => {
|
|
23
|
+
const get2FaConfirmationResultHandler = ref<Get2FaConfirmationResultHandler | null>(null);
|
|
24
|
+
|
|
25
|
+
function setGet2FaConfirmationResultHandler(handler: Get2FaConfirmationResultHandler | null) {
|
|
26
|
+
get2FaConfirmationResultHandler.value = handler;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function get2FaConfirmationResult(
|
|
30
|
+
title?: string,
|
|
31
|
+
verifyingCallback?: VerifyFn
|
|
32
|
+
): Promise<TwoFaConfirmationResult> {
|
|
33
|
+
if (!get2FaConfirmationResultHandler.value) {
|
|
34
|
+
throw new Error('Two factors auth modal API is not initialized');
|
|
35
|
+
}
|
|
36
|
+
return get2FaConfirmationResultHandler.value(title, verifyingCallback);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function initGlobalApi() {
|
|
40
|
+
window.adminforthTwoFaModal = {
|
|
41
|
+
get2FaConfirmationResult,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
get2FaConfirmationResult,
|
|
47
|
+
setGet2FaConfirmationResultHandler,
|
|
48
|
+
initGlobalApi,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const useTwoFactorsAuth = useTwoFactorsAuthApi;
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
<script setup lang="ts">
|
|
102
102
|
|
|
103
103
|
import VOtpInput from 'vue3-otp-input';
|
|
104
|
-
import { ref, nextTick, watch
|
|
104
|
+
import { ref, nextTick, watch } from 'vue';
|
|
105
105
|
import { useUserStore } from '@/stores/user';
|
|
106
106
|
import { useI18n } from 'vue-i18n';
|
|
107
107
|
import { callAdminForthApi } from '@/utils';
|
|
@@ -110,26 +110,16 @@
|
|
|
110
110
|
import { getPasskey } from './utils.js'
|
|
111
111
|
import { useAdminforth } from '@/adminforth';
|
|
112
112
|
import websocket from '@/websocket';
|
|
113
|
+
import { getAdminForthClientId } from '@/utils/clientId';
|
|
113
114
|
import type { AdminUser } from '@/types/Common';
|
|
114
|
-
|
|
115
|
-
type TwoFaConfirmationResult = { mode: 'totp'; result: string } | { mode: 'passkey'; result: Record<string, any> };
|
|
116
|
-
|
|
117
|
-
declare global {
|
|
118
|
-
interface Window {
|
|
119
|
-
adminforthTwoFaModal: {
|
|
120
|
-
get2FaConfirmationResult: (
|
|
121
|
-
title?: string,
|
|
122
|
-
verifyingCallback?: (confirmationResult: string) => Promise<boolean>
|
|
123
|
-
) => Promise<TwoFaConfirmationResult>;
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
115
|
+
import { useTwoFactorsAuthApi, type TwoFaConfirmationResult } from './use2faApi.js';
|
|
127
116
|
const props = defineProps<{
|
|
128
117
|
autoFinishLogin?: boolean
|
|
129
118
|
adminUser?: AdminUser
|
|
130
119
|
}>();
|
|
131
120
|
|
|
132
121
|
const { alert } = useAdminforth();
|
|
122
|
+
const twoFactorsAuthApi = useTwoFactorsAuthApi();
|
|
133
123
|
|
|
134
124
|
const isAwaiting2FAResult = ref(false);
|
|
135
125
|
let allowAddNewSessions = true;
|
|
@@ -150,7 +140,8 @@
|
|
|
150
140
|
watch( props, () => {
|
|
151
141
|
if (props.adminUser) {
|
|
152
142
|
websocket.unsubscribeByPrefix(`/user2fa/`);
|
|
153
|
-
|
|
143
|
+
const user2FaTopic = `/user2fa/${props.adminUser.pk}/${getAdminForthClientId()}`;
|
|
144
|
+
websocket.subscribe(user2FaTopic, async (data: {sessionId: string}) => {
|
|
154
145
|
if (!allowAddNewSessions) {
|
|
155
146
|
alert({message: t('Some process or user tries to add new actions to confirm. Action was blocked'), variant: 'warning'});
|
|
156
147
|
return;
|
|
@@ -162,7 +153,7 @@
|
|
|
162
153
|
}
|
|
163
154
|
try {
|
|
164
155
|
isAwaiting2FAResult.value = true;
|
|
165
|
-
confirmationResult = await
|
|
156
|
+
confirmationResult = await twoFactorsAuthApi.get2FaConfirmationResult();
|
|
166
157
|
} catch (error) {
|
|
167
158
|
if (!isTwoFaCancel(error)) {
|
|
168
159
|
console.error('[AdminForth 2FA] Error during 2FA confirmation', error);
|
|
@@ -188,7 +179,7 @@
|
|
|
188
179
|
}
|
|
189
180
|
allowAddNewSessions = true;
|
|
190
181
|
});
|
|
191
|
-
websocket.subscribe(
|
|
182
|
+
websocket.subscribe(`${user2FaTopic}/resolve`, async (data: {sessionId: string}) => {
|
|
192
183
|
if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
|
|
193
184
|
onCancel();
|
|
194
185
|
sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
|
|
@@ -224,7 +215,6 @@
|
|
|
224
215
|
|
|
225
216
|
const modelShow = ref(false);
|
|
226
217
|
let resolveFn: ((confirmationResult: any) => void) | null = null;
|
|
227
|
-
let verifyingCallback: ((confirmationResult: string) => boolean) | null = null;
|
|
228
218
|
let verifyFn: null | ((confirmationResult: string) => Promise<boolean> | boolean) = null;
|
|
229
219
|
let rejectFn: ((err?: any) => void) | null = null;
|
|
230
220
|
|
|
@@ -233,37 +223,42 @@
|
|
|
233
223
|
}
|
|
234
224
|
|
|
235
225
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
226
|
+
function get2FaConfirmationResult(
|
|
227
|
+
title?: string,
|
|
228
|
+
verifyingCallback?: (confirmationResult: string) => Promise<boolean> | boolean
|
|
229
|
+
): Promise<TwoFaConfirmationResult> {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
(async () => {
|
|
232
|
+
if (modelShow.value) {
|
|
233
|
+
throw new Error(t('Modal is already open'));
|
|
234
|
+
}
|
|
235
|
+
const skipAllowModal = await checkIfSkipAllowModal();
|
|
236
|
+
if (skipAllowModal) {
|
|
237
|
+
resolve({ mode: 'totp', result: '123456' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await checkIfUserHasPasskeys();
|
|
241
|
+
if (title) {
|
|
242
|
+
customDialogTitle.value = title;
|
|
243
|
+
}
|
|
244
|
+
resolveFn = resolve;
|
|
245
|
+
rejectFn = reject;
|
|
246
|
+
verifyFn = verifyingCallback ?? null;
|
|
247
|
+
modelShow.value = true;
|
|
248
|
+
if (modalMode.value === 'totp') {
|
|
249
|
+
await addEventListenerForOTPInput();
|
|
250
|
+
}
|
|
251
|
+
})().catch((error) => {
|
|
252
|
+
if (!isTwoFaCancel(error)) {
|
|
253
|
+
console.error('[AdminForth 2FA] Failed to open 2FA modal', error);
|
|
254
|
+
}
|
|
255
|
+
reject(error);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
twoFactorsAuthApi.setGet2FaConfirmationResultHandler(get2FaConfirmationResult);
|
|
261
|
+
twoFactorsAuthApi.initGlobalApi();
|
|
267
262
|
|
|
268
263
|
const { t } = useI18n();
|
|
269
264
|
const user = useUserStore();
|
|
@@ -274,15 +269,19 @@
|
|
|
274
269
|
const doesUserHavePasskeys = ref(false);
|
|
275
270
|
const modalMode = ref<"totp" | "passkey">("totp");
|
|
276
271
|
const isLoading = ref(false);
|
|
272
|
+
const isFetchingPasskey = ref(false);
|
|
277
273
|
const customDialogTitle = ref("");
|
|
278
274
|
|
|
279
275
|
async function usePasskeyButtonClick() {
|
|
276
|
+
isFetchingPasskey.value = true;
|
|
280
277
|
let passkeyData;
|
|
281
278
|
try {
|
|
282
279
|
passkeyData = await getPasskey();
|
|
283
280
|
} catch (error) {
|
|
284
281
|
onCancel();
|
|
285
282
|
return null;
|
|
283
|
+
} finally {
|
|
284
|
+
isFetchingPasskey.value = false;
|
|
286
285
|
}
|
|
287
286
|
modelShow.value = false;
|
|
288
287
|
const dataToReturn = {
|
|
@@ -291,7 +290,7 @@
|
|
|
291
290
|
}
|
|
292
291
|
customDialogTitle.value = "";
|
|
293
292
|
removeEventListenerForOTPInput();
|
|
294
|
-
resolveFn(dataToReturn);
|
|
293
|
+
resolveFn?.(dataToReturn);
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
function tagOtpInputs() {
|
|
@@ -352,7 +351,7 @@
|
|
|
352
351
|
bindValue.value = '';
|
|
353
352
|
confirmationResult.value?.clearInput();
|
|
354
353
|
removeEventListenerForOTPInput();
|
|
355
|
-
rejectFn("Cancel");
|
|
354
|
+
rejectFn?.("Cancel");
|
|
356
355
|
emit('rejected', new Error('cancelled'));
|
|
357
356
|
emit('closed');
|
|
358
357
|
}
|
|
@@ -438,10 +437,10 @@
|
|
|
438
437
|
((firstEmpty || inputs[0]) as HTMLInputElement).focus();
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
function handleGlobalFocusIn(event) {
|
|
440
|
+
function handleGlobalFocusIn(event: FocusEvent) {
|
|
442
441
|
const inputs = getOtpInputs();
|
|
443
442
|
if (!inputs.length) return;
|
|
444
|
-
const target = event.target;
|
|
443
|
+
const target = event.target as Element | null;
|
|
445
444
|
if (!target) return;
|
|
446
445
|
if (!inputs.includes(target)) {
|
|
447
446
|
requestAnimationFrame(() => {
|
|
@@ -133,11 +133,13 @@
|
|
|
133
133
|
import utc from 'dayjs/plugin/utc';
|
|
134
134
|
import timezone from 'dayjs/plugin/timezone';
|
|
135
135
|
import { useCoreStore } from '@/stores/core';
|
|
136
|
+
import { useTwoFactorsAuth } from './use2faApi';
|
|
136
137
|
|
|
137
138
|
const { t } = useI18n();
|
|
138
139
|
dayjs.extend(utc);
|
|
139
140
|
dayjs.extend(timezone);
|
|
140
141
|
const coreStore = useCoreStore();
|
|
142
|
+
const twoFactorsAuth = useTwoFactorsAuth();
|
|
141
143
|
|
|
142
144
|
interface Passkey {
|
|
143
145
|
id: string;
|
|
@@ -176,8 +178,9 @@
|
|
|
176
178
|
async function startAddPasskey() {
|
|
177
179
|
isFetchingPasskey.value = true;
|
|
178
180
|
try {
|
|
179
|
-
const confirmationResult = await
|
|
180
|
-
|
|
181
|
+
const confirmationResult = await twoFactorsAuth.get2FaConfirmationResult(
|
|
182
|
+
t('To add passkey first verify yourself')
|
|
183
|
+
);
|
|
181
184
|
|
|
182
185
|
if (!confirmationResult) return;
|
|
183
186
|
|
|
@@ -227,7 +230,9 @@
|
|
|
227
230
|
body: { mode: addPasskeyMode.value, confirmationResult },
|
|
228
231
|
});
|
|
229
232
|
if (!response.ok) return { error: response.error ?? t('Verification failed') };
|
|
233
|
+
console.log('Passkey challenge response:', response.data);
|
|
230
234
|
const options = PublicKeyCredential.parseCreationOptionsFromJSON(response.data);
|
|
235
|
+
console.log('Parsed PublicKeyCredentialCreationOptions:', options);
|
|
231
236
|
return { options, challengeId: response.challengeId };
|
|
232
237
|
} catch (error) {
|
|
233
238
|
console.error('Error requesting passkey challenge:', error);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { defineStore } from 'pinia';
|
|
3
|
+
|
|
4
|
+
export type TwoFaConfirmationResult =
|
|
5
|
+
| { mode: 'totp'; result: string }
|
|
6
|
+
| { mode: 'passkey'; result: Record<string, any> };
|
|
7
|
+
|
|
8
|
+
type VerifyFn = (confirmationResult: string) => Promise<boolean> | boolean;
|
|
9
|
+
type Get2FaConfirmationResultHandler = (
|
|
10
|
+
title?: string,
|
|
11
|
+
verifyingCallback?: VerifyFn
|
|
12
|
+
) => Promise<TwoFaConfirmationResult>;
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface Window {
|
|
16
|
+
adminforthTwoFaModal: {
|
|
17
|
+
get2FaConfirmationResult: Get2FaConfirmationResultHandler;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const useTwoFactorsAuthApi = defineStore('twoFactorsAuthApi', () => {
|
|
23
|
+
const get2FaConfirmationResultHandler = ref<Get2FaConfirmationResultHandler | null>(null);
|
|
24
|
+
|
|
25
|
+
function setGet2FaConfirmationResultHandler(handler: Get2FaConfirmationResultHandler | null) {
|
|
26
|
+
get2FaConfirmationResultHandler.value = handler;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function get2FaConfirmationResult(
|
|
30
|
+
title?: string,
|
|
31
|
+
verifyingCallback?: VerifyFn
|
|
32
|
+
): Promise<TwoFaConfirmationResult> {
|
|
33
|
+
if (!get2FaConfirmationResultHandler.value) {
|
|
34
|
+
throw new Error('Two factors auth modal API is not initialized');
|
|
35
|
+
}
|
|
36
|
+
return get2FaConfirmationResultHandler.value(title, verifyingCallback);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function initGlobalApi() {
|
|
40
|
+
window.adminforthTwoFaModal = {
|
|
41
|
+
get2FaConfirmationResult,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
get2FaConfirmationResult,
|
|
47
|
+
setGet2FaConfirmationResultHandler,
|
|
48
|
+
initGlobalApi,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const useTwoFactorsAuth = useTwoFactorsAuthApi;
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { AdminForthPlugin, suggestIfTypo } from "adminforth";
|
|
11
|
+
import * as AdminForthRuntime from "adminforth";
|
|
11
12
|
import { PasskeyService } from "./services/passkeyService.js";
|
|
12
13
|
import { TotpService } from "./services/totpService.js";
|
|
13
14
|
import { verifyMfaConfirmation } from "./services/verifyMfaConfirmation.js";
|
|
@@ -19,6 +20,7 @@ import { createPasskeyHandlers } from "./handlers/createPasskeyHandlers.js";
|
|
|
19
20
|
import { createTwoFaHandlers } from "./handlers/createTwoFaHandlers.js";
|
|
20
21
|
import { parsePeriod } from "./utils/parsePeriod.js";
|
|
21
22
|
import crypto from 'crypto';
|
|
23
|
+
const getRequestWebsocketClientId = AdminForthRuntime.getRequestWebsocketClientId;
|
|
22
24
|
export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
23
25
|
constructor(options) {
|
|
24
26
|
super(options, import.meta.url);
|
|
@@ -77,12 +79,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
77
79
|
}
|
|
78
80
|
verifyAuto(adminUser) {
|
|
79
81
|
return __awaiter(this, void 0, void 0, function* () {
|
|
82
|
+
const websocketClientId = getRequestWebsocketClientId === null || getRequestWebsocketClientId === void 0 ? void 0 : getRequestWebsocketClientId();
|
|
83
|
+
if (!websocketClientId) {
|
|
84
|
+
return { ok: false, error: '2FA auto verification requires an active browser request context' };
|
|
85
|
+
}
|
|
80
86
|
const sessionId = crypto.randomUUID();
|
|
81
87
|
const jwt = this.adminforth.auth.issueJWT({ sessionId, adminUserPk: adminUser.pk }, 'auto2FA', '5m');
|
|
82
88
|
const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
|
|
83
|
-
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
|
|
89
|
+
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}`, { sessionId: jwt });
|
|
84
90
|
const result = yield resultPromise;
|
|
85
|
-
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}
|
|
91
|
+
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}/resolve`, { sessionId: jwt });
|
|
86
92
|
return result;
|
|
87
93
|
});
|
|
88
94
|
}
|
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AdminForthPlugin, suggestIfTypo, HttpExtra } from "adminforth";
|
|
2
|
+
import * as AdminForthRuntime from "adminforth";
|
|
2
3
|
import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, IAdminForthHttpResponse } from "adminforth";
|
|
3
4
|
import { PluginOptions } from "./types.js"
|
|
4
5
|
import { PasskeyService } from "./services/passkeyService.js";
|
|
@@ -13,6 +14,10 @@ import { createTwoFaHandlers } from "./handlers/createTwoFaHandlers.js";
|
|
|
13
14
|
import { parsePeriod } from "./utils/parsePeriod.js";
|
|
14
15
|
import crypto from 'crypto';
|
|
15
16
|
|
|
17
|
+
const getRequestWebsocketClientId = (AdminForthRuntime as typeof AdminForthRuntime & {
|
|
18
|
+
getRequestWebsocketClientId?: () => string | undefined;
|
|
19
|
+
}).getRequestWebsocketClientId;
|
|
20
|
+
|
|
16
21
|
export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
17
22
|
options: PluginOptions;
|
|
18
23
|
adminforth: IAdminForth;
|
|
@@ -87,15 +92,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
public async verifyAuto(adminUser: AdminUser) {
|
|
95
|
+
const websocketClientId = getRequestWebsocketClientId?.();
|
|
96
|
+
if (!websocketClientId) {
|
|
97
|
+
return { ok: false, error: '2FA auto verification requires an active browser request context' };
|
|
98
|
+
}
|
|
90
99
|
const sessionId = crypto.randomUUID();
|
|
91
100
|
const jwt = this.adminforth.auth.issueJWT({sessionId, adminUserPk: adminUser.pk}, 'auto2FA', '5m');
|
|
92
101
|
const resultPromise = this.waitForResponse(jwt, 5 * 60 * 1000);
|
|
93
102
|
|
|
94
|
-
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
|
|
103
|
+
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}`, { sessionId: jwt });
|
|
95
104
|
|
|
96
105
|
const result = await resultPromise;
|
|
97
106
|
|
|
98
|
-
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}
|
|
107
|
+
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}/${websocketClientId}/resolve`, { sessionId: jwt });
|
|
99
108
|
|
|
100
109
|
return result;
|
|
101
110
|
}
|
package/package.json
CHANGED