@adminforth/two-factors-auth 1.10.4 → 1.10.6
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 +2 -2
- package/custom/TwoFactorsConfirmation.vue +23 -10
- package/custom/TwoFactorsPasskeysSettings.vue +2 -2
- package/custom/TwoFactorsSetup.vue +11 -1
- package/dist/custom/TwoFactorsConfirmation.vue +23 -10
- package/dist/custom/TwoFactorsPasskeysSettings.vue +2 -2
- package/dist/custom/TwoFactorsSetup.vue +11 -1
- package/dist/index.js +47 -15
- package/index.ts +60 -16
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -12,5 +12,5 @@ custom/package-lock.json
|
|
|
12
12
|
custom/package.json
|
|
13
13
|
custom/tsconfig.json
|
|
14
14
|
|
|
15
|
-
sent
|
|
16
|
-
total size is 57,
|
|
15
|
+
sent 58,387 bytes received 153 bytes 117,080.00 bytes/sec
|
|
16
|
+
total size is 57,814 speedup is 0.99
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<script setup lang="ts">
|
|
76
76
|
|
|
77
|
-
import { onMounted, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
77
|
+
import { onMounted, nextTick, onBeforeUnmount, ref, watch, onBeforeMount } from 'vue';
|
|
78
78
|
import { useCoreStore } from '@/stores/core';
|
|
79
79
|
import { useUserStore } from '@/stores/user';
|
|
80
80
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
@@ -95,6 +95,17 @@
|
|
|
95
95
|
const router = useRouter();
|
|
96
96
|
const codeError = ref(null);
|
|
97
97
|
|
|
98
|
+
onBeforeMount(() => {
|
|
99
|
+
if (localStorage.getItem('isAuthorized') === 'true') {
|
|
100
|
+
coreStore.fetchMenuAndResource();
|
|
101
|
+
if (route.query.next) {
|
|
102
|
+
router.push(route.query.next.toString());
|
|
103
|
+
} else {
|
|
104
|
+
router.push({ name: 'home' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
98
109
|
const handleOnComplete = (value) => {
|
|
99
110
|
sendCode(value, 'TOTP', null);
|
|
100
111
|
};
|
|
@@ -121,16 +132,18 @@
|
|
|
121
132
|
const isPasskeysSupported = ref(false);
|
|
122
133
|
|
|
123
134
|
onMounted(async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
if (localStorage.getItem('isAuthorized') !== 'true') {
|
|
136
|
+
await nextTick();
|
|
137
|
+
await isCMAAvailable();
|
|
138
|
+
tagOtpInputs();
|
|
139
|
+
if (isPasskeysSupported.value === true) {
|
|
140
|
+
checkIfUserHasPasskeys();
|
|
141
|
+
}
|
|
142
|
+
document.addEventListener('focusin', handleGlobalFocusIn, true);
|
|
143
|
+
focusFirstAvailableOtpInput();
|
|
144
|
+
const rootEl = otpRoot.value;
|
|
145
|
+
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
|
|
129
146
|
}
|
|
130
|
-
document.addEventListener('focusin', handleGlobalFocusIn, true);
|
|
131
|
-
focusFirstAvailableOtpInput();
|
|
132
|
-
const rootEl = otpRoot.value;
|
|
133
|
-
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
|
|
134
147
|
});
|
|
135
148
|
|
|
136
149
|
watch(route, (newRoute) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-3xl text-gray-900 font-semibold
|
|
3
|
-
<p>Passkeys</p>
|
|
2
|
+
<div class="text-3xl text-gray-900 font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
|
|
3
|
+
<p class="flex items-start justify-start leading-none">Passkeys</p>
|
|
4
4
|
<div class="flex flex-col items-end">
|
|
5
5
|
<Table
|
|
6
6
|
class="mt-4 w-full"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
|
|
68
68
|
<script setup lang="ts">
|
|
69
69
|
|
|
70
|
-
import { onMounted, onBeforeUnmount, nextTick, ref, watchEffect,computed,watch } from 'vue';
|
|
70
|
+
import { onMounted, onBeforeUnmount, nextTick, ref, watchEffect,computed,watch, onBeforeMount } from 'vue';
|
|
71
71
|
import { useCoreStore } from '@/stores/core';
|
|
72
72
|
import { useUserStore } from '@/stores/user';
|
|
73
73
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
@@ -86,6 +86,16 @@ import ErrorMessage from '@/components/ErrorMessage.vue';
|
|
|
86
86
|
const { t } = useI18n();
|
|
87
87
|
const route = useRoute()
|
|
88
88
|
|
|
89
|
+
onBeforeMount(() => {
|
|
90
|
+
if (localStorage.getItem('isAuthorized') === 'true') {
|
|
91
|
+
coreStore.fetchMenuAndResource();
|
|
92
|
+
if (route.query.next) {
|
|
93
|
+
router.push(route.query.next.toString());
|
|
94
|
+
} else {
|
|
95
|
+
router.push({ name: 'home' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
89
99
|
|
|
90
100
|
const code = ref(null);
|
|
91
101
|
const codeError = ref(null);
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<script setup lang="ts">
|
|
76
76
|
|
|
77
|
-
import { onMounted, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
77
|
+
import { onMounted, nextTick, onBeforeUnmount, ref, watch, onBeforeMount } from 'vue';
|
|
78
78
|
import { useCoreStore } from '@/stores/core';
|
|
79
79
|
import { useUserStore } from '@/stores/user';
|
|
80
80
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
@@ -95,6 +95,17 @@
|
|
|
95
95
|
const router = useRouter();
|
|
96
96
|
const codeError = ref(null);
|
|
97
97
|
|
|
98
|
+
onBeforeMount(() => {
|
|
99
|
+
if (localStorage.getItem('isAuthorized') === 'true') {
|
|
100
|
+
coreStore.fetchMenuAndResource();
|
|
101
|
+
if (route.query.next) {
|
|
102
|
+
router.push(route.query.next.toString());
|
|
103
|
+
} else {
|
|
104
|
+
router.push({ name: 'home' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
98
109
|
const handleOnComplete = (value) => {
|
|
99
110
|
sendCode(value, 'TOTP', null);
|
|
100
111
|
};
|
|
@@ -121,16 +132,18 @@
|
|
|
121
132
|
const isPasskeysSupported = ref(false);
|
|
122
133
|
|
|
123
134
|
onMounted(async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
if (localStorage.getItem('isAuthorized') !== 'true') {
|
|
136
|
+
await nextTick();
|
|
137
|
+
await isCMAAvailable();
|
|
138
|
+
tagOtpInputs();
|
|
139
|
+
if (isPasskeysSupported.value === true) {
|
|
140
|
+
checkIfUserHasPasskeys();
|
|
141
|
+
}
|
|
142
|
+
document.addEventListener('focusin', handleGlobalFocusIn, true);
|
|
143
|
+
focusFirstAvailableOtpInput();
|
|
144
|
+
const rootEl = otpRoot.value;
|
|
145
|
+
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
|
|
129
146
|
}
|
|
130
|
-
document.addEventListener('focusin', handleGlobalFocusIn, true);
|
|
131
|
-
focusFirstAvailableOtpInput();
|
|
132
|
-
const rootEl = otpRoot.value;
|
|
133
|
-
rootEl && rootEl.addEventListener('focusout', handleFocusOut, true);
|
|
134
147
|
});
|
|
135
148
|
|
|
136
149
|
watch(route, (newRoute) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-3xl text-gray-900 font-semibold
|
|
3
|
-
<p>Passkeys</p>
|
|
2
|
+
<div class="text-3xl text-gray-900 font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
|
|
3
|
+
<p class="flex items-start justify-start leading-none">Passkeys</p>
|
|
4
4
|
<div class="flex flex-col items-end">
|
|
5
5
|
<Table
|
|
6
6
|
class="mt-4 w-full"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
|
|
68
68
|
<script setup lang="ts">
|
|
69
69
|
|
|
70
|
-
import { onMounted, onBeforeUnmount, nextTick, ref, watchEffect,computed,watch } from 'vue';
|
|
70
|
+
import { onMounted, onBeforeUnmount, nextTick, ref, watchEffect,computed,watch, onBeforeMount } from 'vue';
|
|
71
71
|
import { useCoreStore } from '@/stores/core';
|
|
72
72
|
import { useUserStore } from '@/stores/user';
|
|
73
73
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
@@ -86,6 +86,16 @@ import ErrorMessage from '@/components/ErrorMessage.vue';
|
|
|
86
86
|
const { t } = useI18n();
|
|
87
87
|
const route = useRoute()
|
|
88
88
|
|
|
89
|
+
onBeforeMount(() => {
|
|
90
|
+
if (localStorage.getItem('isAuthorized') === 'true') {
|
|
91
|
+
coreStore.fetchMenuAndResource();
|
|
92
|
+
if (route.query.next) {
|
|
93
|
+
router.push(route.query.next.toString());
|
|
94
|
+
} else {
|
|
95
|
+
router.push({ name: 'home' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
89
99
|
|
|
90
100
|
const code = ref(null);
|
|
91
101
|
const codeError = ref(null);
|
package/dist/index.js
CHANGED
|
@@ -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 { AdminForthPlugin, Filters } from "adminforth";
|
|
10
|
+
import { AdminForthPlugin, Filters, suggestIfTypo } from "adminforth";
|
|
11
11
|
import twofactor from 'node-2fa';
|
|
12
12
|
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
|
|
13
13
|
import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
@@ -71,7 +71,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
71
71
|
const response = JSON.parse(body.response);
|
|
72
72
|
try {
|
|
73
73
|
if (settingsOrigin !== expectedOrigin) {
|
|
74
|
-
throw new Error(
|
|
74
|
+
throw new Error(`Origin mismatch. Allowed in settings: ${settingsOrigin}, received from client: ${expectedOrigin}`);
|
|
75
75
|
}
|
|
76
76
|
const cred = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, response.id)]);
|
|
77
77
|
if (!cred) {
|
|
@@ -92,7 +92,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
92
92
|
const { verified, authenticationInfo } = yield verifyAuthenticationResponse({
|
|
93
93
|
response,
|
|
94
94
|
expectedChallenge,
|
|
95
|
-
expectedOrigin,
|
|
95
|
+
expectedOrigin: settingsOrigin,
|
|
96
96
|
expectedRPID,
|
|
97
97
|
credential: {
|
|
98
98
|
id: cred[this.options.passkeys.credentialIdFieldName],
|
|
@@ -125,11 +125,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
125
125
|
const customPages = this.adminforth.config.customization.customPages;
|
|
126
126
|
customPages.push({
|
|
127
127
|
path: '/confirm2fa',
|
|
128
|
-
component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: {
|
|
128
|
+
component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: { sidebarAndHeader: "none", suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
|
|
129
129
|
});
|
|
130
130
|
customPages.push({
|
|
131
131
|
path: '/setup2fa',
|
|
132
|
-
component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA',
|
|
132
|
+
component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', sidebarAndHeader: "none", suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
|
|
133
133
|
});
|
|
134
134
|
const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || [];
|
|
135
135
|
everyPageBottomInjections.push({ file: this.componentPath('TwoFAModal.vue'), meta: {} });
|
|
@@ -161,9 +161,23 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
161
161
|
if (!this.options.passkeys.credentialIdFieldName) {
|
|
162
162
|
throw new Error('Passkeys credentialIdFieldName is required');
|
|
163
163
|
}
|
|
164
|
+
const credentialResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
165
|
+
const credentialIDField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialIdFieldName);
|
|
166
|
+
if (!credentialIDField) {
|
|
167
|
+
const similar = suggestIfTypo(credentialResource.columns.map(c => c.name), this.options.passkeys.credentialIdFieldName);
|
|
168
|
+
throw new Error(`Passkeys credentialIdFieldName '${this.options.passkeys.credentialIdFieldName}' not found in resource '${this.options.passkeys.credentialResourceID}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
|
|
169
|
+
}
|
|
170
|
+
credentialIDField.backendOnly = true;
|
|
164
171
|
if (!this.options.passkeys.credentialMetaFieldName) {
|
|
165
172
|
throw new Error('Passkeys credentialMetaFieldName is required');
|
|
166
173
|
}
|
|
174
|
+
const metaResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialMetaFieldName);
|
|
175
|
+
const metaField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialMetaFieldName);
|
|
176
|
+
if (!metaField) {
|
|
177
|
+
const similar = suggestIfTypo(metaResource.columns.map(c => c.name), this.options.passkeys.credentialMetaFieldName);
|
|
178
|
+
throw new Error(`Passkeys credentialMetaFieldName '${this.options.passkeys.credentialMetaFieldName}' not found in resource '${this.options.passkeys.credentialMetaFieldName}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
metaField.backendOnly = true;
|
|
167
181
|
if (!this.options.passkeys.credentialUserIdFieldName) {
|
|
168
182
|
throw new Error('Passkeys credentialUserIdFieldName is required');
|
|
169
183
|
}
|
|
@@ -429,7 +443,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
429
443
|
userVerification: ((_m = this.options.passkeys) === null || _m === void 0 ? void 0 : _m.settings.authenticatorSelection.userVerification) || "required"
|
|
430
444
|
},
|
|
431
445
|
});
|
|
432
|
-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, '
|
|
446
|
+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, 'registerTempPasskeyChallenge', '10m');
|
|
433
447
|
this.adminforth.auth.setCustomCookie({ response, payload: { name: "registerPasskeyTemporaryJWT", value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
|
|
434
448
|
return { ok: true, data: options };
|
|
435
449
|
})
|
|
@@ -444,7 +458,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
444
458
|
if (!passkeysCookies) {
|
|
445
459
|
return { error: 'Passkey token is required' };
|
|
446
460
|
}
|
|
447
|
-
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, '
|
|
461
|
+
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'registerTempPasskeyChallenge', false);
|
|
448
462
|
if (!decodedPasskeysCookies) {
|
|
449
463
|
return { error: 'Invalid passkey token' };
|
|
450
464
|
}
|
|
@@ -556,11 +570,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
556
570
|
path: `/plugin/passkeys/deletePasskey`,
|
|
557
571
|
noAuth: false,
|
|
558
572
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
559
|
-
const
|
|
560
|
-
if (!
|
|
573
|
+
const credentialID = body.passkeyId;
|
|
574
|
+
if (!credentialID) {
|
|
561
575
|
return { ok: false, error: 'Passkey ID is required' };
|
|
562
576
|
}
|
|
563
|
-
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName,
|
|
577
|
+
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
564
578
|
if (!passkeyRecord) {
|
|
565
579
|
return { ok: false, error: 'Passkey not found' };
|
|
566
580
|
}
|
|
@@ -569,9 +583,18 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
569
583
|
if (!credResource) {
|
|
570
584
|
throw new Error('Credential resource not found.');
|
|
571
585
|
}
|
|
586
|
+
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
|
|
587
|
+
if (!credResourcePKColumn) {
|
|
588
|
+
throw new Error('Credential resource primary key not found.');
|
|
589
|
+
}
|
|
590
|
+
const credResourcePKName = credResourcePKColumn.name;
|
|
591
|
+
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
592
|
+
if (!passkeyRecord) {
|
|
593
|
+
return { ok: false, error: 'Passkey not found' };
|
|
594
|
+
}
|
|
572
595
|
yield this.adminforth.deleteResourceRecord({
|
|
573
596
|
resource: credResource,
|
|
574
|
-
recordId:
|
|
597
|
+
recordId: passkeyRecord[credResourcePKName],
|
|
575
598
|
record: passkeyRecord,
|
|
576
599
|
adminUser: adminUser,
|
|
577
600
|
});
|
|
@@ -587,15 +610,24 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
587
610
|
path: `/plugin/passkeys/renamePasskey`,
|
|
588
611
|
noAuth: false,
|
|
589
612
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
590
|
-
const
|
|
613
|
+
const credentialID = body.passkeyId;
|
|
591
614
|
const newName = body.newName;
|
|
592
|
-
if (!
|
|
615
|
+
if (!credentialID) {
|
|
593
616
|
return { ok: false, error: 'Passkey ID is required' };
|
|
594
617
|
}
|
|
595
618
|
if (!newName) {
|
|
596
619
|
return { ok: false, error: 'New name is required' };
|
|
597
620
|
}
|
|
598
|
-
const
|
|
621
|
+
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
622
|
+
if (!credResource) {
|
|
623
|
+
throw new Error('Credential resource not found.');
|
|
624
|
+
}
|
|
625
|
+
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
|
|
626
|
+
if (!credResourcePKColumn) {
|
|
627
|
+
throw new Error('Credential resource primary key not found.');
|
|
628
|
+
}
|
|
629
|
+
const credResourcePKName = credResourcePKColumn.name;
|
|
630
|
+
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
599
631
|
if (!passkeyRecord) {
|
|
600
632
|
return { ok: false, error: 'Passkey not found' };
|
|
601
633
|
}
|
|
@@ -609,7 +641,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
609
641
|
}
|
|
610
642
|
yield this.adminforth.updateResourceRecord({
|
|
611
643
|
resource: credResource,
|
|
612
|
-
recordId:
|
|
644
|
+
recordId: passkeyRecord[credResourcePKName],
|
|
613
645
|
oldRecord: passkeyRecord,
|
|
614
646
|
record: newRecord,
|
|
615
647
|
adminUser: adminUser
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminForthPlugin, Filters } from "adminforth";
|
|
1
|
+
import { AdminForthPlugin, Filters, suggestIfTypo } from "adminforth";
|
|
2
2
|
import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, BeforeLoginConfirmationFunction, IAdminForthHttpResponse } from "adminforth";
|
|
3
3
|
import twofactor from 'node-2fa';
|
|
4
4
|
import { PluginOptions } from "./types.js"
|
|
@@ -80,7 +80,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
80
80
|
const response = JSON.parse(body.response);
|
|
81
81
|
try {
|
|
82
82
|
if (settingsOrigin !== expectedOrigin) {
|
|
83
|
-
throw new Error(
|
|
83
|
+
throw new Error(`Origin mismatch. Allowed in settings: ${settingsOrigin}, received from client: ${expectedOrigin}`);
|
|
84
84
|
}
|
|
85
85
|
const cred = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, response.id)]);
|
|
86
86
|
if (!cred) {
|
|
@@ -101,7 +101,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
101
101
|
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
|
|
102
102
|
response,
|
|
103
103
|
expectedChallenge,
|
|
104
|
-
expectedOrigin,
|
|
104
|
+
expectedOrigin: settingsOrigin,
|
|
105
105
|
expectedRPID,
|
|
106
106
|
credential: {
|
|
107
107
|
id: cred[this.options.passkeys.credentialIdFieldName],
|
|
@@ -134,11 +134,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
134
134
|
const customPages = this.adminforth.config.customization.customPages
|
|
135
135
|
customPages.push({
|
|
136
136
|
path:'/confirm2fa',
|
|
137
|
-
component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: {
|
|
137
|
+
component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: { sidebarAndHeader: "none", suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
|
|
138
138
|
})
|
|
139
139
|
customPages.push({
|
|
140
140
|
path:'/setup2fa',
|
|
141
|
-
component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA',
|
|
141
|
+
component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', sidebarAndHeader: "none", suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
|
|
142
142
|
})
|
|
143
143
|
const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || []
|
|
144
144
|
everyPageBottomInjections.push({ file: this.componentPath('TwoFAModal.vue'), meta: {} })
|
|
@@ -173,9 +173,36 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
173
173
|
if (!this.options.passkeys.credentialIdFieldName) {
|
|
174
174
|
throw new Error('Passkeys credentialIdFieldName is required');
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
const credentialResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
178
|
+
const credentialIDField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialIdFieldName);
|
|
179
|
+
if ( !credentialIDField ) {
|
|
180
|
+
const similar = suggestIfTypo(credentialResource.columns.map(c => c.name), this.options.passkeys.credentialIdFieldName);
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Passkeys credentialIdFieldName '${this.options.passkeys.credentialIdFieldName}' not found in resource '${this.options.passkeys.credentialResourceID}'. ${
|
|
183
|
+
similar ? `Did you mean '${similar}'?` : ''
|
|
184
|
+
}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
credentialIDField.backendOnly = true;
|
|
188
|
+
|
|
176
189
|
if (!this.options.passkeys.credentialMetaFieldName) {
|
|
177
190
|
throw new Error('Passkeys credentialMetaFieldName is required');
|
|
178
191
|
}
|
|
192
|
+
|
|
193
|
+
const metaResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialMetaFieldName);
|
|
194
|
+
const metaField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialMetaFieldName);
|
|
195
|
+
if ( !metaField ) {
|
|
196
|
+
const similar = suggestIfTypo(metaResource.columns.map(c => c.name), this.options.passkeys.credentialMetaFieldName);
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Passkeys credentialMetaFieldName '${this.options.passkeys.credentialMetaFieldName}' not found in resource '${this.options.passkeys.credentialMetaFieldName}'. ${
|
|
199
|
+
similar ? `Did you mean '${similar}'?` : ''
|
|
200
|
+
}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
metaField.backendOnly = true;
|
|
204
|
+
|
|
205
|
+
|
|
179
206
|
if (!this.options.passkeys.credentialUserIdFieldName) {
|
|
180
207
|
throw new Error('Passkeys credentialUserIdFieldName is required');
|
|
181
208
|
}
|
|
@@ -455,7 +482,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
455
482
|
userVerification: this.options.passkeys?.settings.authenticatorSelection.userVerification || "required"
|
|
456
483
|
},
|
|
457
484
|
});
|
|
458
|
-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, '
|
|
485
|
+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, 'registerTempPasskeyChallenge', '10m');
|
|
459
486
|
this.adminforth.auth.setCustomCookie({response, payload: {name: "registerPasskeyTemporaryJWT", value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true}});
|
|
460
487
|
return { ok: true, data: options };
|
|
461
488
|
}
|
|
@@ -469,7 +496,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
469
496
|
if (!passkeysCookies) {
|
|
470
497
|
return { error: 'Passkey token is required' };
|
|
471
498
|
}
|
|
472
|
-
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, '
|
|
499
|
+
const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'registerTempPasskeyChallenge', false);
|
|
473
500
|
if (!decodedPasskeysCookies) {
|
|
474
501
|
return { error: 'Invalid passkey token' };
|
|
475
502
|
}
|
|
@@ -584,12 +611,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
584
611
|
path: `/plugin/passkeys/deletePasskey`,
|
|
585
612
|
noAuth: false,
|
|
586
613
|
handler: async ({body, adminUser }) => {
|
|
587
|
-
const
|
|
588
|
-
if (!
|
|
614
|
+
const credentialID = body.passkeyId;
|
|
615
|
+
if (!credentialID) {
|
|
589
616
|
return { ok: false, error: 'Passkey ID is required' };
|
|
590
617
|
}
|
|
591
618
|
|
|
592
|
-
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName,
|
|
619
|
+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
593
620
|
if (!passkeyRecord) {
|
|
594
621
|
return { ok: false, error: 'Passkey not found' };
|
|
595
622
|
}
|
|
@@ -598,9 +625,18 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
598
625
|
if (!credResource) {
|
|
599
626
|
throw new Error('Credential resource not found.');
|
|
600
627
|
}
|
|
628
|
+
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
|
|
629
|
+
if (!credResourcePKColumn) {
|
|
630
|
+
throw new Error('Credential resource primary key not found.');
|
|
631
|
+
}
|
|
632
|
+
const credResourcePKName = credResourcePKColumn.name;
|
|
633
|
+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
634
|
+
if (!passkeyRecord) {
|
|
635
|
+
return { ok: false, error: 'Passkey not found' };
|
|
636
|
+
}
|
|
601
637
|
await this.adminforth.deleteResourceRecord({
|
|
602
638
|
resource: credResource,
|
|
603
|
-
recordId:
|
|
639
|
+
recordId: passkeyRecord[credResourcePKName],
|
|
604
640
|
record: passkeyRecord,
|
|
605
641
|
adminUser: adminUser,
|
|
606
642
|
});
|
|
@@ -616,16 +652,24 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
616
652
|
path: `/plugin/passkeys/renamePasskey`,
|
|
617
653
|
noAuth: false,
|
|
618
654
|
handler: async ({body, adminUser }) => {
|
|
619
|
-
const
|
|
655
|
+
const credentialID = body.passkeyId;
|
|
620
656
|
const newName = body.newName;
|
|
621
|
-
if (!
|
|
657
|
+
if (!credentialID) {
|
|
622
658
|
return { ok: false, error: 'Passkey ID is required' };
|
|
623
659
|
}
|
|
624
660
|
if (!newName) {
|
|
625
661
|
return { ok: false, error: 'New name is required' };
|
|
626
662
|
}
|
|
627
|
-
|
|
628
|
-
|
|
663
|
+
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
664
|
+
if (!credResource) {
|
|
665
|
+
throw new Error('Credential resource not found.');
|
|
666
|
+
}
|
|
667
|
+
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
|
|
668
|
+
if (!credResourcePKColumn) {
|
|
669
|
+
throw new Error('Credential resource primary key not found.');
|
|
670
|
+
}
|
|
671
|
+
const credResourcePKName = credResourcePKColumn.name;
|
|
672
|
+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
629
673
|
if (!passkeyRecord) {
|
|
630
674
|
return { ok: false, error: 'Passkey not found' };
|
|
631
675
|
}
|
|
@@ -639,7 +683,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
639
683
|
}
|
|
640
684
|
await this.adminforth.updateResourceRecord({
|
|
641
685
|
resource: credResource,
|
|
642
|
-
recordId:
|
|
686
|
+
recordId: passkeyRecord[credResourcePKName],
|
|
643
687
|
oldRecord: passkeyRecord,
|
|
644
688
|
record: newRecord,
|
|
645
689
|
adminUser: adminUser
|