@adminforth/two-factors-auth 2.2.2 → 2.3.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/LoginWithPasskeyButton.vue +55 -0
- package/dist/custom/LoginWithPasskeyButton.vue +55 -0
- package/dist/index.js +108 -20
- package/index.ts +100 -1
- package/package.json +1 -1
- package/types.ts +10 -0
package/build.log
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
sending incremental file list
|
|
6
6
|
custom/
|
|
7
|
+
custom/LoginWithPasskeyButton.vue
|
|
7
8
|
custom/TwoFAModal.vue
|
|
8
9
|
custom/TwoFactorsConfirmation.vue
|
|
9
10
|
custom/TwoFactorsPasskeysSettings.vue
|
|
@@ -14,5 +15,5 @@ custom/package.json
|
|
|
14
15
|
custom/tsconfig.json
|
|
15
16
|
custom/utils.js
|
|
16
17
|
|
|
17
|
-
sent
|
|
18
|
-
total size is
|
|
18
|
+
sent 271,266 bytes received 210 bytes 542,952.00 bytes/sec
|
|
19
|
+
total size is 270,439 speedup is 1.00
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<Button
|
|
4
|
+
class="w-full !py-1"
|
|
5
|
+
@click="handleLoginWithPasskey"
|
|
6
|
+
>
|
|
7
|
+
<IconShieldOutline class="w-7 h-8" />
|
|
8
|
+
<span class="ml-2">Continue with Passkey</span>
|
|
9
|
+
</Button>
|
|
10
|
+
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
|
|
17
|
+
import { getPasskey } from "./utils.js";
|
|
18
|
+
import { callAdminForthApi } from '@/utils';
|
|
19
|
+
import { useUserStore } from '@/stores/user';
|
|
20
|
+
import { Button } from '@/afcl';
|
|
21
|
+
import adminforth from '@/adminforth';
|
|
22
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
23
|
+
|
|
24
|
+
const userStore = useUserStore();
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
async function handleLoginWithPasskey() {
|
|
28
|
+
const passkeyOptions = await getPasskey();
|
|
29
|
+
try {
|
|
30
|
+
const resp = await callAdminForthApi({
|
|
31
|
+
method: 'POST',
|
|
32
|
+
path: '/plugin/twofa/confirmLoginWithPasskey',
|
|
33
|
+
body: {
|
|
34
|
+
passkeyResponse: passkeyOptions
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if ( resp.error ) {
|
|
38
|
+
console.error("Login failed:", resp.error);
|
|
39
|
+
adminforth.alert({message: 'Error: ' + resp.error, variant: 'warning'});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ( resp.allowedLogin === true ) {
|
|
44
|
+
await userStore.finishLogin();
|
|
45
|
+
} else if (resp.redirectTo) {
|
|
46
|
+
router.push(resp.redirectTo);
|
|
47
|
+
} else {
|
|
48
|
+
console.error("Login not allowed:", resp.error);
|
|
49
|
+
adminforth.alert({message: 'Error: ' + (resp.error || 'Login not allowed'), variant: 'warning'});
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Error during passkey login:", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<Button
|
|
4
|
+
class="w-full !py-1"
|
|
5
|
+
@click="handleLoginWithPasskey"
|
|
6
|
+
>
|
|
7
|
+
<IconShieldOutline class="w-7 h-8" />
|
|
8
|
+
<span class="ml-2">Continue with Passkey</span>
|
|
9
|
+
</Button>
|
|
10
|
+
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
|
|
17
|
+
import { getPasskey } from "./utils.js";
|
|
18
|
+
import { callAdminForthApi } from '@/utils';
|
|
19
|
+
import { useUserStore } from '@/stores/user';
|
|
20
|
+
import { Button } from '@/afcl';
|
|
21
|
+
import adminforth from '@/adminforth';
|
|
22
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
23
|
+
|
|
24
|
+
const userStore = useUserStore();
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
async function handleLoginWithPasskey() {
|
|
28
|
+
const passkeyOptions = await getPasskey();
|
|
29
|
+
try {
|
|
30
|
+
const resp = await callAdminForthApi({
|
|
31
|
+
method: 'POST',
|
|
32
|
+
path: '/plugin/twofa/confirmLoginWithPasskey',
|
|
33
|
+
body: {
|
|
34
|
+
passkeyResponse: passkeyOptions
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if ( resp.error ) {
|
|
38
|
+
console.error("Login failed:", resp.error);
|
|
39
|
+
adminforth.alert({message: 'Error: ' + resp.error, variant: 'warning'});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ( resp.allowedLogin === true ) {
|
|
44
|
+
await userStore.finishLogin();
|
|
45
|
+
} else if (resp.redirectTo) {
|
|
46
|
+
router.push(resp.redirectTo);
|
|
47
|
+
} else {
|
|
48
|
+
console.error("Login not allowed:", resp.error);
|
|
49
|
+
adminforth.alert({message: 'Error: ' + (resp.error || 'Login not allowed'), variant: 'warning'});
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Error during passkey login:", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
163
163
|
slug: 'passkeys',
|
|
164
164
|
component: this.componentPath('TwoFactorsPasskeysSettings.vue'),
|
|
165
165
|
});
|
|
166
|
+
if (this.options.passkeys.allowLoginWithPasskeys !== false) {
|
|
167
|
+
this.options.passkeys.allowLoginWithPasskeys = true;
|
|
168
|
+
if (!this.adminforth.config.customization.loginPageInjections) {
|
|
169
|
+
this.adminforth.config.customization.loginPageInjections = { underInputs: [], panelHeader: [] };
|
|
170
|
+
}
|
|
171
|
+
this.adminforth.config.customization.loginPageInjections.underInputs.push({ file: this.componentPath('LoginWithPasskeyButton.vue'), meta: {} });
|
|
172
|
+
}
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
@@ -248,7 +255,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
248
255
|
const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
|
|
249
256
|
const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
|
|
250
257
|
beforeLoginConfirmationArray.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response, extra }) {
|
|
251
|
-
var _b;
|
|
258
|
+
var _b, _c;
|
|
259
|
+
if (((_b = extra === null || extra === void 0 ? void 0 : extra.body) === null || _b === void 0 ? void 0 : _b.loginAllowedByPasskeyDirectSignIn) === true) {
|
|
260
|
+
return { body: { loginAllowed: true }, ok: true };
|
|
261
|
+
}
|
|
252
262
|
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName];
|
|
253
263
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
|
|
254
264
|
const brandName = adminforth.config.customization.brandName;
|
|
@@ -259,7 +269,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
259
269
|
const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.usersResourceId);
|
|
260
270
|
const authPk = authResource.columns.find((col) => col.primaryKey).name;
|
|
261
271
|
const userPk = adminUser.dbUser[authPk];
|
|
262
|
-
const rememberMe = ((
|
|
272
|
+
const rememberMe = ((_c = extra === null || extra === void 0 ? void 0 : extra.body) === null || _c === void 0 ? void 0 : _c.rememberMe) || false;
|
|
263
273
|
const rememberMeDays = rememberMe ? adminforth.config.auth.rememberMeDays || 30 : 1;
|
|
264
274
|
let newSecret = null;
|
|
265
275
|
const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
|
|
@@ -376,29 +386,107 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
376
386
|
})
|
|
377
387
|
});
|
|
378
388
|
server.endpoint({
|
|
379
|
-
method:
|
|
380
|
-
path:
|
|
389
|
+
method: 'POST',
|
|
390
|
+
path: `/plugin/twofa/confirmLoginWithPasskey`,
|
|
381
391
|
noAuth: true,
|
|
382
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, cookies, headers, requestUrl, query }) {
|
|
393
|
+
if (this.options.passkeys.allowLoginWithPasskeys !== true) {
|
|
394
|
+
return { error: 'Login with passkeys is not allowed' };
|
|
395
|
+
}
|
|
396
|
+
const passkeyResponse = body.passkeyResponse;
|
|
397
|
+
if (!passkeyResponse) {
|
|
398
|
+
return { error: 'Passkey response is required' };
|
|
399
|
+
}
|
|
400
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "passkeyLoginTemporaryJWT" });
|
|
401
|
+
if (!totpTemporaryJWT) {
|
|
402
|
+
return { error: 'Authentication session is expired. Please, try again' };
|
|
403
|
+
}
|
|
404
|
+
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'tempLoginPasskeyChallenge', false);
|
|
388
405
|
if (!decoded) {
|
|
389
|
-
return {
|
|
406
|
+
return { error: 'Authentication session is expired. Please, try again' };
|
|
390
407
|
}
|
|
391
|
-
|
|
392
|
-
|
|
408
|
+
let parsedPasskeyResponse;
|
|
409
|
+
try {
|
|
410
|
+
parsedPasskeyResponse = JSON.parse(passkeyResponse.response);
|
|
393
411
|
}
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
status: "ok",
|
|
397
|
-
skipAllowed: decoded.userCanSkipSetup,
|
|
398
|
-
};
|
|
412
|
+
catch (e) {
|
|
413
|
+
return { error: 'Malformed passkey response' };
|
|
399
414
|
}
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
const credential_id = parsedPasskeyResponse.id;
|
|
416
|
+
if (!credential_id) {
|
|
417
|
+
return { error: 'Credential ID is required' };
|
|
418
|
+
}
|
|
419
|
+
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credential_id)]);
|
|
420
|
+
if (!passkeyRecord) {
|
|
421
|
+
return { error: 'Passkey not found' };
|
|
422
|
+
}
|
|
423
|
+
const userPk = passkeyRecord[this.options.passkeys.credentialUserIdFieldName];
|
|
424
|
+
if (!userPk) {
|
|
425
|
+
return { error: 'User ID not found in passkey record' };
|
|
426
|
+
}
|
|
427
|
+
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
428
|
+
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
429
|
+
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
430
|
+
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
431
|
+
const user = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, userPk)]);
|
|
432
|
+
if (!user) {
|
|
433
|
+
return { error: 'User not found' };
|
|
434
|
+
}
|
|
435
|
+
const verificationResult = yield this.verifyPasskeyResponse(passkeyResponse, userPk, decoded);
|
|
436
|
+
if (!verificationResult.ok || !verificationResult.passkeyConfirmed) {
|
|
437
|
+
return { error: 'Passkey verification failed' };
|
|
438
|
+
}
|
|
439
|
+
const username = user[this.adminforth.config.auth.usernameField];
|
|
440
|
+
const adminUser = {
|
|
441
|
+
dbUser: user,
|
|
442
|
+
pk: user.id,
|
|
443
|
+
username,
|
|
444
|
+
};
|
|
445
|
+
const toReturn = { allowedLogin: true, error: '' };
|
|
446
|
+
yield this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response, {
|
|
447
|
+
headers,
|
|
448
|
+
cookies,
|
|
449
|
+
requestUrl,
|
|
450
|
+
query,
|
|
451
|
+
body: {
|
|
452
|
+
loginAllowedByPasskeyDirectSignIn: true
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
if (toReturn.allowedLogin === true) {
|
|
456
|
+
this.adminforth.auth.setAuthCookie({
|
|
457
|
+
response,
|
|
458
|
+
username,
|
|
459
|
+
pk: user.id,
|
|
460
|
+
expireInDays: this.options.passkeys.rememberDaysAfterPasskeyLogin ? this.options.passkeys.rememberDaysAfterPasskeyLogin : this.adminforth.config.auth.rememberMeDays,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return toReturn;
|
|
464
|
+
})
|
|
465
|
+
}),
|
|
466
|
+
server.endpoint({
|
|
467
|
+
method: "GET",
|
|
468
|
+
path: "/plugin/twofa/skip-allow",
|
|
469
|
+
noAuth: true,
|
|
470
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
|
|
471
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({
|
|
472
|
+
cookies,
|
|
473
|
+
name: `2FaTemporaryJWT`,
|
|
474
|
+
});
|
|
475
|
+
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "temp2FA");
|
|
476
|
+
if (!decoded) {
|
|
477
|
+
return { status: "error", message: "Invalid token" };
|
|
478
|
+
}
|
|
479
|
+
if (!decoded.newSecret) {
|
|
480
|
+
return { status: "ok", skipAllowed: false };
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
return {
|
|
484
|
+
status: "ok",
|
|
485
|
+
skipAllowed: decoded.userCanSkipSetup,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
489
|
+
});
|
|
402
490
|
server.endpoint({
|
|
403
491
|
method: 'POST',
|
|
404
492
|
path: `/plugin/twofa/verify`,
|
package/index.ts
CHANGED
|
@@ -174,6 +174,14 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
174
174
|
slug: 'passkeys',
|
|
175
175
|
component: this.componentPath('TwoFactorsPasskeysSettings.vue'),
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
if ( this.options.passkeys.allowLoginWithPasskeys !== false ) {
|
|
179
|
+
this.options.passkeys.allowLoginWithPasskeys = true;
|
|
180
|
+
if ( !this.adminforth.config.customization.loginPageInjections ) {
|
|
181
|
+
this.adminforth.config.customization.loginPageInjections = { underInputs: [], panelHeader: [] };
|
|
182
|
+
}
|
|
183
|
+
this.adminforth.config.customization.loginPageInjections.underInputs.push({ file: this.componentPath('LoginWithPasskeyButton.vue'), meta: {} });
|
|
184
|
+
}
|
|
177
185
|
}
|
|
178
186
|
}
|
|
179
187
|
|
|
@@ -281,6 +289,9 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
281
289
|
const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
|
|
282
290
|
beforeLoginConfirmationArray.push(
|
|
283
291
|
async({ adminUser, response, extra }: { adminUser: AdminUser, response: IAdminForthHttpResponse, extra?: any} )=> {
|
|
292
|
+
if (extra?.body?.loginAllowedByPasskeyDirectSignIn === true) {
|
|
293
|
+
return { body: { loginAllowed: true }, ok: true };
|
|
294
|
+
}
|
|
284
295
|
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName]
|
|
285
296
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField]
|
|
286
297
|
const brandName = adminforth.config.customization.brandName;
|
|
@@ -299,7 +310,6 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
299
310
|
if (!userNeeds2FA){
|
|
300
311
|
return { body:{loginAllowed: true}, ok: true}
|
|
301
312
|
}
|
|
302
|
-
|
|
303
313
|
const userCanSkipSetup = this.options.usersFilterToAllowSkipSetup ? this.options.usersFilterToAllowSkipSetup(adminUser) : false;
|
|
304
314
|
|
|
305
315
|
if (!secret){
|
|
@@ -413,6 +423,95 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
413
423
|
}
|
|
414
424
|
}
|
|
415
425
|
})
|
|
426
|
+
server.endpoint({
|
|
427
|
+
method: 'POST',
|
|
428
|
+
path: `/plugin/twofa/confirmLoginWithPasskey`,
|
|
429
|
+
noAuth: true,
|
|
430
|
+
handler: async ({ body, response, cookies, headers, requestUrl, query }) => {
|
|
431
|
+
if ( this.options.passkeys.allowLoginWithPasskeys !== true ) {
|
|
432
|
+
return { error: 'Login with passkeys is not allowed' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const passkeyResponse = body.passkeyResponse;
|
|
436
|
+
if (!passkeyResponse) {
|
|
437
|
+
return { error: 'Passkey response is required' };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({cookies: cookies, name: "passkeyLoginTemporaryJWT"});
|
|
441
|
+
if (!totpTemporaryJWT) {
|
|
442
|
+
return { error: 'Authentication session is expired. Please, try again' }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const decoded = await this.adminforth.auth.verify(totpTemporaryJWT, 'tempLoginPasskeyChallenge', false);
|
|
446
|
+
if (!decoded) {
|
|
447
|
+
return { error: 'Authentication session is expired. Please, try again' }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let parsedPasskeyResponse;
|
|
451
|
+
try {
|
|
452
|
+
parsedPasskeyResponse = JSON.parse(passkeyResponse.response);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
return { error: 'Malformed passkey response' };
|
|
455
|
+
}
|
|
456
|
+
const credential_id = parsedPasskeyResponse.id;
|
|
457
|
+
if (!credential_id) {
|
|
458
|
+
return { error: 'Credential ID is required' };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const passkeyRecord = await this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credential_id)]);
|
|
462
|
+
if (!passkeyRecord) {
|
|
463
|
+
return { error: 'Passkey not found' };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const userPk = passkeyRecord[this.options.passkeys.credentialUserIdFieldName];
|
|
467
|
+
if (!userPk) {
|
|
468
|
+
return { error: 'User ID not found in passkey record' };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
472
|
+
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
473
|
+
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
474
|
+
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
475
|
+
const user = await this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, userPk)]);
|
|
476
|
+
if (!user) {
|
|
477
|
+
return { error: 'User not found' };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const verificationResult = await this.verifyPasskeyResponse(passkeyResponse, userPk, decoded);
|
|
481
|
+
if (!verificationResult.ok || !verificationResult.passkeyConfirmed) {
|
|
482
|
+
return { error: 'Passkey verification failed' };
|
|
483
|
+
}
|
|
484
|
+
const username = user[this.adminforth.config.auth.usernameField];
|
|
485
|
+
|
|
486
|
+
const adminUser = {
|
|
487
|
+
dbUser: user,
|
|
488
|
+
pk: user.id,
|
|
489
|
+
username,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const toReturn = { allowedLogin: true, error: '' };
|
|
493
|
+
|
|
494
|
+
await this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response, {
|
|
495
|
+
headers,
|
|
496
|
+
cookies,
|
|
497
|
+
requestUrl,
|
|
498
|
+
query,
|
|
499
|
+
body: {
|
|
500
|
+
loginAllowedByPasskeyDirectSignIn: true
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
if ( toReturn.allowedLogin === true ) {
|
|
505
|
+
this.adminforth.auth.setAuthCookie({
|
|
506
|
+
response,
|
|
507
|
+
username,
|
|
508
|
+
pk: user.id,
|
|
509
|
+
expireInDays: this.options.passkeys.rememberDaysAfterPasskeyLogin ? this.options.passkeys.rememberDaysAfterPasskeyLogin : this.adminforth.config.auth.rememberMeDays,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return toReturn;
|
|
513
|
+
}
|
|
514
|
+
}),
|
|
416
515
|
server.endpoint({
|
|
417
516
|
method: "GET",
|
|
418
517
|
path: "/plugin/twofa/skip-allow",
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -26,6 +26,16 @@ export type PluginOptions = {
|
|
|
26
26
|
credentialIdFieldName: string,
|
|
27
27
|
credentialMetaFieldName: string,
|
|
28
28
|
credentialUserIdFieldName: string,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Allow login with Passkeys even if 2FA is not set up. Default is true.
|
|
32
|
+
*/
|
|
33
|
+
allowLoginWithPasskeys?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remember user for number of days after login with Passkey, so that user won't be challenged with 2FA on every login.
|
|
37
|
+
*/
|
|
38
|
+
rememberDaysAfterPasskeyLogin?: number;
|
|
29
39
|
/**
|
|
30
40
|
* Period between showing alert suggesting to set up Passkeys if not set up yet.
|
|
31
41
|
*/
|