@adminforth/two-factors-auth 1.10.6 → 1.11.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 +113 -19
- package/custom/TwoFactorsConfirmation.vue +13 -62
- package/custom/TwoFactorsPasskeysSettings.vue +102 -25
- package/custom/TwoFactorsSetup.vue +1 -1
- package/custom/utils.js +106 -0
- package/dist/custom/TwoFAModal.vue +113 -19
- package/dist/custom/TwoFactorsConfirmation.vue +13 -62
- package/dist/custom/TwoFactorsPasskeysSettings.vue +102 -25
- package/dist/custom/TwoFactorsSetup.vue +1 -1
- package/dist/custom/utils.js +106 -0
- package/dist/index.js +52 -25
- package/index.ts +50 -20
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-3xl text-
|
|
2
|
+
<div class="text-3xl text-lightBreadcrumbsText dark:text-darkBreadcrumbsText font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
|
|
3
3
|
<p class="flex items-start justify-start leading-none">Passkeys</p>
|
|
4
4
|
<div class="flex flex-col items-end">
|
|
5
5
|
<Table
|
|
@@ -17,17 +17,23 @@
|
|
|
17
17
|
class="w-96"
|
|
18
18
|
:buttons="[
|
|
19
19
|
{ label: 'Save', onclick: (dialog) => { renamePasskey(item.id, passkeysNewName); dialog.hide(); } },
|
|
20
|
-
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700 dark:border-gray-900'}, onclick: (dialog) => {passkeysNewName = ''; dialog.hide();} },
|
|
20
|
+
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700 dark:border-gray-900 dark:hover:!border-gray-800 dark:focus:!ring-gray-800'}, onclick: (dialog) => {passkeysNewName = ''; dialog.hide();} },
|
|
21
21
|
]"
|
|
22
22
|
header="Edit Passkey"
|
|
23
23
|
>
|
|
24
24
|
<template #trigger>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
<Tooltip>
|
|
26
|
+
<div
|
|
27
|
+
@click="passkeysNewName = ''"
|
|
28
|
+
class="w-7 h-7 flex items-center justify-center rounded-md text-blue-500 hover:text-blue-700 cursor-pointer"
|
|
29
|
+
>
|
|
30
|
+
<IconPenSolid class="w-5 h-5" />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<template #tooltip>
|
|
34
|
+
Rename passkey
|
|
35
|
+
</template>
|
|
36
|
+
</Tooltip>
|
|
31
37
|
</template>
|
|
32
38
|
<div>
|
|
33
39
|
<p>Enter new passkey name:</p>
|
|
@@ -42,17 +48,22 @@
|
|
|
42
48
|
<Dialog
|
|
43
49
|
class="w-96"
|
|
44
50
|
:buttons="[
|
|
45
|
-
{ label: 'Delete', options: {class: 'bg-red-700 !text-white hover:!bg-red-600 focus:ring-2 focus:ring-red-500'}, onclick: (dialog) => { deletePasskey(item.id); dialog.hide(); } },
|
|
46
|
-
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200'}, onclick: (dialog) => dialog.hide() },
|
|
51
|
+
{ label: 'Delete', options: {class: 'bg-red-700 !text-white hover:!bg-red-600 focus:ring-2 focus:ring-red-500 dark:!bg-red-700 dark:hover:!bg-red-600 dark:focus:ring-red-500 dark:!border-red-800'}, onclick: (dialog) => { deletePasskey(item.id); dialog.hide(); } },
|
|
52
|
+
{ label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!text-white dark:!bg-gray-700 dark:hover:!bg-gray-800 dark:!border-gray-600 dark:focus:!ring-gray-800'}, onclick: (dialog) => dialog.hide() },
|
|
47
53
|
]"
|
|
48
54
|
header="Delete Passkey"
|
|
49
55
|
>
|
|
50
56
|
<template #trigger>
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
<Tooltip>
|
|
58
|
+
<div class="w-7 h-7 flex items-center justify-center rounded-md text-red-500 hover:text-red-700 cursor-pointer">
|
|
59
|
+
<IconTrashBinSolid
|
|
60
|
+
class="w-5 h-5"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<template #tooltip>
|
|
64
|
+
Delete passkey
|
|
65
|
+
</template>
|
|
66
|
+
</Tooltip>
|
|
56
67
|
</template>
|
|
57
68
|
<div>
|
|
58
69
|
<p>Are you sure you want to delete this passkey?</p>
|
|
@@ -66,9 +77,9 @@
|
|
|
66
77
|
</template>
|
|
67
78
|
</Table>
|
|
68
79
|
<div class="flex space-x-4 mt-4" v-if="isInitialFinished">
|
|
69
|
-
<ButtonGroup :solidColor="true">
|
|
80
|
+
<ButtonGroup :solidColor="true" v-if="isFetchingPasskey === false">
|
|
70
81
|
<template #button:Profile>
|
|
71
|
-
<div class="flex px-4 py-2" @click="
|
|
82
|
+
<div class="flex px-4 py-2" @click="addPasskeyStartAction()">
|
|
72
83
|
<IconPlusOutline class="w-5 h-5 me-2"/>
|
|
73
84
|
<p>{{ addPasskeyMode === 'platform' ? 'Add Local Passkey' : 'Add External Passkey' }}</p>
|
|
74
85
|
</div>
|
|
@@ -79,6 +90,7 @@
|
|
|
79
90
|
</div>
|
|
80
91
|
</template>
|
|
81
92
|
</ButtonGroup>
|
|
93
|
+
<p v-else class="flex items-center justify-center gap-2 text-base">Processing <Spinner class="w-4 h-4 inline-block" /></p>
|
|
82
94
|
</div>
|
|
83
95
|
<div v-if="isCardsVisible" id="cards-container" class="w-80 mt-2 border-gray-400 p-2 bg-white rounded-lg shadow-md flex flex-col space-y-2">
|
|
84
96
|
<div v-if="isPasskeySupported" class="flex justify-between gap-4" :class="!isPasskeySupported ? 'opacity-50 pointer-events-none' : ''">
|
|
@@ -127,6 +139,29 @@
|
|
|
127
139
|
</div>
|
|
128
140
|
</div>
|
|
129
141
|
</div>
|
|
142
|
+
<Dialog
|
|
143
|
+
ref="confirmDialog"
|
|
144
|
+
class="w-96"
|
|
145
|
+
:click-to-close-outside="false"
|
|
146
|
+
:buttons="[]"
|
|
147
|
+
>
|
|
148
|
+
<div class="flex flex-col">
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
class="text-lightDialogCloseButton bg-transparent hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover"
|
|
152
|
+
@click="{ isFetchingPasskey = false; fetchedOptions = null; closeDialog(); }"
|
|
153
|
+
>
|
|
154
|
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
155
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
|
156
|
+
</svg>
|
|
157
|
+
<span class="sr-only">Close dialog</span>
|
|
158
|
+
</button>
|
|
159
|
+
<div class="flex items-center justify-center space-y-4">
|
|
160
|
+
Now you can add a passkey.
|
|
161
|
+
</div>
|
|
162
|
+
<Button class="mt-4" @click="addPasskeyFinishAction(fetchedOptions); closeDialog();">Add Passkey</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</Dialog>
|
|
130
165
|
</div>
|
|
131
166
|
</template>
|
|
132
167
|
|
|
@@ -135,7 +170,7 @@
|
|
|
135
170
|
import { callAdminForthApi } from '@/utils';
|
|
136
171
|
import adminforth from '@/adminforth';
|
|
137
172
|
import { onMounted, ref, Ref, onBeforeUnmount } from 'vue';
|
|
138
|
-
import { Card, Dialog, ButtonGroup, Tooltip } from '@/afcl'
|
|
173
|
+
import { Card, Dialog, ButtonGroup, Tooltip, Spinner, Button } from '@/afcl'
|
|
139
174
|
import { IconTrashBinSolid, IconPenSolid, IconPlusOutline, IconCaretDownSolid, IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
|
|
140
175
|
import dayjs from 'dayjs';
|
|
141
176
|
import utc from 'dayjs/plugin/utc';
|
|
@@ -152,6 +187,19 @@
|
|
|
152
187
|
const addPasskeyMode: Ref<'platform' | 'cross-platform'> = ref('platform');
|
|
153
188
|
const authenticatorAttachment = ref<'platform' | 'cross-platform' | 'both'>('platform');
|
|
154
189
|
const isInitialFinished = ref(false);
|
|
190
|
+
const isFetchingPasskey = ref(false);
|
|
191
|
+
|
|
192
|
+
const confirmDialog = ref(null);
|
|
193
|
+
|
|
194
|
+
const openDialog = () => {
|
|
195
|
+
confirmDialog.value.open();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const closeDialog = () => {
|
|
199
|
+
confirmDialog.value.close();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const fetchedOptions = ref(null);
|
|
155
203
|
|
|
156
204
|
onMounted(async () => {
|
|
157
205
|
await getPasskeys();
|
|
@@ -179,13 +227,39 @@
|
|
|
179
227
|
}
|
|
180
228
|
}
|
|
181
229
|
|
|
182
|
-
async function
|
|
183
|
-
|
|
230
|
+
async function addPasskeyStartAction() {
|
|
231
|
+
isFetchingPasskey.value = true;
|
|
232
|
+
let confirmationResult;
|
|
233
|
+
try {
|
|
234
|
+
confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(undefined, "To add passkey first verify yourself");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
isFetchingPasskey.value = false;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!confirmationResult) {
|
|
240
|
+
isFetchingPasskey.value = false;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const { options } = await fetchInformationFromTheBackend(confirmationResult);
|
|
244
|
+
if (!options ) {
|
|
245
|
+
isFetchingPasskey.value = false;
|
|
246
|
+
adminforth.alert({message: 'Verification failed.', variant: 'warning'});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
fetchedOptions.value = options;
|
|
250
|
+
openDialog();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function addPasskeyFinishAction(options) {
|
|
184
254
|
const creationResult = await callWebAuthn(options);
|
|
185
255
|
if (!creationResult) {
|
|
256
|
+
isFetchingPasskey.value = false;
|
|
257
|
+
fetchedOptions.value = null;
|
|
186
258
|
return;
|
|
187
259
|
}
|
|
188
|
-
|
|
260
|
+
fetchedOptions.value = null;
|
|
261
|
+
finishRegisteringPasskey(creationResult);
|
|
262
|
+
isFetchingPasskey.value = false;
|
|
189
263
|
}
|
|
190
264
|
|
|
191
265
|
async function getPasskeys() {
|
|
@@ -267,20 +341,24 @@
|
|
|
267
341
|
}
|
|
268
342
|
}
|
|
269
343
|
|
|
270
|
-
async function fetchInformationFromTheBackend() {
|
|
344
|
+
async function fetchInformationFromTheBackend(confirmationResult) {
|
|
271
345
|
let response;
|
|
272
346
|
try {
|
|
273
347
|
response = await callAdminForthApi({
|
|
274
348
|
path: `/plugin/passkeys/registerPasskeyRequest`,
|
|
275
349
|
method: 'POST',
|
|
276
350
|
body: {
|
|
277
|
-
mode: addPasskeyMode.value
|
|
351
|
+
mode: addPasskeyMode.value,
|
|
352
|
+
confirmationResult: confirmationResult
|
|
278
353
|
},
|
|
279
354
|
});
|
|
280
355
|
} catch (error) {
|
|
281
356
|
console.error('Error fetching passkeys info:', error);
|
|
282
357
|
return;
|
|
283
358
|
}
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
284
362
|
const _options = response.data;
|
|
285
363
|
const challengeId = response.challengeId;
|
|
286
364
|
const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
|
|
@@ -303,7 +381,7 @@
|
|
|
303
381
|
return result;
|
|
304
382
|
}
|
|
305
383
|
|
|
306
|
-
async function finishRegisteringPasskey(credential: any
|
|
384
|
+
async function finishRegisteringPasskey(credential: any) {
|
|
307
385
|
let res
|
|
308
386
|
try {
|
|
309
387
|
res = await callAdminForthApi({
|
|
@@ -312,7 +390,6 @@
|
|
|
312
390
|
body: {
|
|
313
391
|
credential: credential,
|
|
314
392
|
origin: window.location.origin,
|
|
315
|
-
challengeId: challengeId,
|
|
316
393
|
},
|
|
317
394
|
});
|
|
318
395
|
} catch (error) {
|
|
@@ -80,7 +80,7 @@ import VOtpInput from "vue3-otp-input";
|
|
|
80
80
|
import adminforth from '@/adminforth';
|
|
81
81
|
import { useI18n } from 'vue-i18n';
|
|
82
82
|
import { useRoute } from 'vue-router';
|
|
83
|
-
import { handlePasskeyAlert } from './
|
|
83
|
+
import { handlePasskeyAlert } from './utils.js';
|
|
84
84
|
import ErrorMessage from '@/components/ErrorMessage.vue';
|
|
85
85
|
|
|
86
86
|
const { t } = useI18n();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { callAdminForthApi } from '@/utils';
|
|
2
|
+
|
|
3
|
+
export function handlePasskeyAlert(propSuggestionPeriod, router) {
|
|
4
|
+
const currentDate = Date.now();
|
|
5
|
+
window.localStorage.removeItem('suggestionPeriod');
|
|
6
|
+
window.localStorage.setItem('suggestionPeriod', propSuggestionPeriod);
|
|
7
|
+
let suggestionPeriod = window.localStorage.getItem('suggestionPeriod');
|
|
8
|
+
let lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
|
|
9
|
+
let suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
10
|
+
if ( !lastSuggestionDate ) {
|
|
11
|
+
window.localStorage.setItem('lastSuggestionDate', currentDate.toString());
|
|
12
|
+
lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
|
|
13
|
+
}
|
|
14
|
+
if ( !suggestPasskey ) {
|
|
15
|
+
window.localStorage.setItem('suggestPasskey', 'true');
|
|
16
|
+
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
17
|
+
}
|
|
18
|
+
if ( currentDate - parseInt(lastSuggestionDate) > parseInt(suggestionPeriod) ) {
|
|
19
|
+
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
20
|
+
if (suggestPasskey !== 'true'){
|
|
21
|
+
if ( suggestPasskey === 'false' || !suggestPasskey ) {
|
|
22
|
+
window.localStorage.setItem('suggestPasskey', 'true');
|
|
23
|
+
} else if ( suggestPasskey !== 'never' ) {
|
|
24
|
+
window.localStorage.setItem('suggestPasskey', 'false');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
suggestPasskey = window.localStorage.getItem('suggestPasskey');
|
|
29
|
+
|
|
30
|
+
if ( suggestPasskey === 'true' ) {
|
|
31
|
+
adminforth.alert({
|
|
32
|
+
message: 'Do you want to add passkey?',
|
|
33
|
+
variant: 'info',
|
|
34
|
+
buttons: [
|
|
35
|
+
{ value: 'yes', label: 'Add passkey' },
|
|
36
|
+
{ value: 'later', label: 'Later' },
|
|
37
|
+
{ value: 'never', label: 'Never' },
|
|
38
|
+
],
|
|
39
|
+
timeout: 'unlimited'
|
|
40
|
+
}).then((value) => {
|
|
41
|
+
switch (value) {
|
|
42
|
+
case 'yes':
|
|
43
|
+
router.push({ name: 'settings', params: { page: 'passkeys' } });
|
|
44
|
+
break;
|
|
45
|
+
case 'later':
|
|
46
|
+
window.localStorage.setItem('suggestPasskey', 'false');
|
|
47
|
+
break;
|
|
48
|
+
case 'never':
|
|
49
|
+
window.localStorage.setItem('suggestPasskey', 'never');
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
window.localStorage.setItem('suggestPasskey', 'false');
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getPasskey() {
|
|
60
|
+
const { _options } = await createSignInRequest();
|
|
61
|
+
const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
|
|
62
|
+
const credential = await authenticate(options);
|
|
63
|
+
if (!credential) {
|
|
64
|
+
isFetchingPasskey.value = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const result = JSON.stringify(credential);
|
|
68
|
+
const passkeyOptions = {
|
|
69
|
+
response: result,
|
|
70
|
+
origin: window.location.origin,
|
|
71
|
+
};
|
|
72
|
+
return passkeyOptions;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function createSignInRequest() {
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await callAdminForthApi({
|
|
79
|
+
path: `/plugin/passkeys/signInRequest`,
|
|
80
|
+
method: 'POST',
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error creating sign-in request:', error);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (response.ok === true) {
|
|
87
|
+
return { _options: response.data, challengeId: response.challengeId };
|
|
88
|
+
} else {
|
|
89
|
+
adminforth.alert({message: 'Error creating sign-in request.', variant: 'warning'});
|
|
90
|
+
codeError.value = 'Error creating sign-in request.';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function authenticate(options) {
|
|
95
|
+
try {
|
|
96
|
+
const abortController = new AbortController();
|
|
97
|
+
const credential = await navigator.credentials.get({
|
|
98
|
+
publicKey: options,
|
|
99
|
+
signal: abortController.signal,
|
|
100
|
+
});
|
|
101
|
+
return credential;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
adminforth.alert({message: 'Error during authentication', variant: 'warning'});
|
|
104
|
+
codeError.value = 'Error during authentication.';
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,30 +19,48 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
19
19
|
instanceUniqueRepresentation(pluginOptions) {
|
|
20
20
|
return `single`;
|
|
21
21
|
}
|
|
22
|
-
verify(
|
|
22
|
+
verify(confirmationResult, opts) {
|
|
23
23
|
return __awaiter(this, void 0, void 0, function* () {
|
|
24
24
|
var _a, _b, _c, _d;
|
|
25
|
-
if (!
|
|
26
|
-
return { error: "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
if (!confirmationResult)
|
|
26
|
+
return { error: "Confirmation result is required" };
|
|
27
|
+
if (confirmationResult.mode === "totp") {
|
|
28
|
+
const code = confirmationResult.result;
|
|
29
|
+
const authRes = this.adminforth.config.resources
|
|
30
|
+
.find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
31
|
+
if (!authRes)
|
|
32
|
+
return { error: "Auth resource not found" };
|
|
33
|
+
const connector = this.adminforth.connectors[authRes.dataSource];
|
|
34
|
+
const pkName = (_a = authRes.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name;
|
|
35
|
+
if (!pkName)
|
|
36
|
+
return { error: "Primary key not found on auth resource" };
|
|
37
|
+
const pk = (_b = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _b !== void 0 ? _b : (_d = (_c = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _c === void 0 ? void 0 : _c.dbUser) === null || _d === void 0 ? void 0 : _d[pkName];
|
|
38
|
+
if (!pk)
|
|
39
|
+
return { error: "User PK is required" };
|
|
40
|
+
const user = yield connector.getRecordByPrimaryKey(authRes, pk);
|
|
41
|
+
if (!user)
|
|
42
|
+
return { error: "User not found" };
|
|
43
|
+
const secret = user[this.options.twoFaSecretFieldName];
|
|
44
|
+
if (!secret)
|
|
45
|
+
return { error: "2FA is not set up for this user" };
|
|
46
|
+
const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
|
|
47
|
+
return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
|
|
48
|
+
}
|
|
49
|
+
else if (confirmationResult.mode === "passkey") {
|
|
50
|
+
const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: opts.cookies, name: `passkeyLoginTemporaryJWT` });
|
|
51
|
+
if (!passkeysCookies) {
|
|
52
|
+
return { error: 'Passkey token is required' };
|
|
53
|
+
}
|
|
54
|
+
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
55
|
+
if (!decodedPasskeysCookies) {
|
|
56
|
+
return { error: 'Invalid passkey' };
|
|
57
|
+
}
|
|
58
|
+
const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies);
|
|
59
|
+
if (verificationResult.ok && verificationResult.passkeyConfirmed) {
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
return { error: "Invalid passkey" };
|
|
63
|
+
}
|
|
46
64
|
});
|
|
47
65
|
}
|
|
48
66
|
parsePeriod(period) {
|
|
@@ -326,7 +344,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
326
344
|
if (!passkeysCookies) {
|
|
327
345
|
return { error: 'Passkey token is required' };
|
|
328
346
|
}
|
|
329
|
-
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, '
|
|
347
|
+
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
330
348
|
if (!decodedPasskeysCookies) {
|
|
331
349
|
return { error: 'Invalid passkey' };
|
|
332
350
|
}
|
|
@@ -400,9 +418,18 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
400
418
|
method: 'POST',
|
|
401
419
|
path: `/plugin/passkeys/registerPasskeyRequest`,
|
|
402
420
|
noAuth: false,
|
|
403
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
421
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
|
|
404
422
|
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
405
423
|
const mode = body === null || body === void 0 ? void 0 : body.mode;
|
|
424
|
+
const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
|
|
425
|
+
const verificationResult = yield this.verify(confirmationResult, {
|
|
426
|
+
adminUser: adminUser,
|
|
427
|
+
userPk: adminUser.pk,
|
|
428
|
+
cookies: cookies
|
|
429
|
+
});
|
|
430
|
+
if (!verificationResult || !('ok' in verificationResult)) {
|
|
431
|
+
return { ok: false, error: 'Verification failed' };
|
|
432
|
+
}
|
|
406
433
|
const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
|
|
407
434
|
const rp = {
|
|
408
435
|
name: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.rp.name) || this.adminforth.config.customization.brandName,
|
|
@@ -530,7 +557,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
530
557
|
rpID: (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.rp.id,
|
|
531
558
|
userVerification: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.authenticatorSelection.userVerification) || "required"
|
|
532
559
|
});
|
|
533
|
-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, '
|
|
560
|
+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempLoginPasskeyChallenge', '10m');
|
|
534
561
|
this.adminforth.auth.setCustomCookie({ response, payload: { name: `passkeyLoginTemporaryJWT`, value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
|
|
535
562
|
return { ok: true, data: options };
|
|
536
563
|
}
|
package/index.ts
CHANGED
|
@@ -27,31 +27,50 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
public async verify(
|
|
30
|
-
|
|
31
|
-
opts?: { adminUser?: AdminUser; userPk?: string }
|
|
30
|
+
confirmationResult: Record<string, any>,
|
|
31
|
+
opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any }
|
|
32
32
|
): Promise<{ ok: true } | { error: string }> {
|
|
33
|
-
if (!
|
|
33
|
+
if (!confirmationResult) return { error: "Confirmation result is required" };
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (confirmationResult.mode === "totp") {
|
|
36
|
+
const code = confirmationResult.result;
|
|
37
|
+
const authRes = this.adminforth.config.resources
|
|
38
|
+
.find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
if (!authRes) return { error: "Auth resource not found" };
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const connector = this.adminforth.connectors[authRes.dataSource];
|
|
43
|
+
const pkName = authRes.columns.find(c => c.primaryKey)?.name;
|
|
44
|
+
if (!pkName) return { error: "Primary key not found on auth resource" };
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const pk = opts?.userPk ?? opts?.adminUser?.dbUser?.[pkName];
|
|
47
|
+
if (!pk) return { error: "User PK is required" };
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const user = await connector.getRecordByPrimaryKey(authRes, pk);
|
|
50
|
+
if (!user) return { error: "User not found" };
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const secret = user[this.options.twoFaSecretFieldName];
|
|
53
|
+
if (!secret) return { error: "2FA is not set up for this user" };
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
|
|
56
|
+
return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
|
|
57
|
+
} else if (confirmationResult.mode === "passkey") {
|
|
58
|
+
const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: opts.cookies, name: `passkeyLoginTemporaryJWT`});
|
|
59
|
+
if (!passkeysCookies) {
|
|
60
|
+
return { error: 'Passkey token is required' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
64
|
+
if (!decodedPasskeysCookies) {
|
|
65
|
+
return { error: 'Invalid passkey' };
|
|
66
|
+
}
|
|
67
|
+
const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies );
|
|
68
|
+
|
|
69
|
+
if (verificationResult.ok && verificationResult.passkeyConfirmed) {
|
|
70
|
+
return { ok: true }
|
|
71
|
+
}
|
|
72
|
+
return { error: "Invalid passkey" }
|
|
73
|
+
}
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
public parsePeriod(period?: string): number {
|
|
@@ -365,7 +384,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
365
384
|
if (!passkeysCookies) {
|
|
366
385
|
return { error: 'Passkey token is required' };
|
|
367
386
|
}
|
|
368
|
-
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, '
|
|
387
|
+
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
369
388
|
if (!decodedPasskeysCookies) {
|
|
370
389
|
return { error: 'Invalid passkey' };
|
|
371
390
|
}
|
|
@@ -440,8 +459,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
440
459
|
method: 'POST',
|
|
441
460
|
path: `/plugin/passkeys/registerPasskeyRequest`,
|
|
442
461
|
noAuth: false,
|
|
443
|
-
handler: async ({ body, adminUser, response }) => {
|
|
462
|
+
handler: async ({ body, adminUser, response, cookies }) => {
|
|
444
463
|
const mode = body?.mode;
|
|
464
|
+
|
|
465
|
+
const confirmationResult = body?.confirmationResult;
|
|
466
|
+
const verificationResult = await this.verify(confirmationResult, {
|
|
467
|
+
adminUser: adminUser,
|
|
468
|
+
userPk: adminUser.pk,
|
|
469
|
+
cookies: cookies
|
|
470
|
+
});
|
|
471
|
+
if ( !verificationResult || !('ok' in verificationResult) ) {
|
|
472
|
+
return { ok: false, error: 'Verification failed' };
|
|
473
|
+
}
|
|
474
|
+
|
|
445
475
|
const settingsOrigin = this.options.passkeys?.settings.expectedOrigin;
|
|
446
476
|
const rp = {
|
|
447
477
|
name: this.options.passkeys?.settings.rp.name || this.adminforth.config.customization.brandName,
|
|
@@ -574,7 +604,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
574
604
|
rpID: this.options.passkeys?.settings.rp.id,
|
|
575
605
|
userVerification: this.options.passkeys?.settings.authenticatorSelection.userVerification || "required"
|
|
576
606
|
});
|
|
577
|
-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, '
|
|
607
|
+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempLoginPasskeyChallenge', '10m');
|
|
578
608
|
this.adminforth.auth.setCustomCookie({response, payload: {name: `passkeyLoginTemporaryJWT`, value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true}});
|
|
579
609
|
return { ok: true, data: options };
|
|
580
610
|
} catch (e) {
|