@adonis-agora/authkit-server 0.71.0 → 0.72.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/host/views/mfa-challenge.edge +8 -3
- package/build/src/accounts/account_store.d.ts +8 -0
- package/build/src/accounts/lucid_store/mfa.js +9 -1
- package/build/src/host/controllers/interaction_controller.d.ts +28 -0
- package/build/src/host/controllers/interaction_controller.js +119 -62
- package/package.json +1 -1
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
@end
|
|
18
18
|
|
|
19
19
|
{{-- Step-up sem MFA enrolado: bloqueia o login e instrui a configurar o MFA;
|
|
20
|
-
não renderiza o campo de código (não há 2º fator a desafiar).
|
|
21
|
-
|
|
20
|
+
não renderiza o campo de código (não há 2º fator a desafiar).
|
|
21
|
+
`totpAvailable === false` = a conta só tem passkey, sem app autenticador
|
|
22
|
+
confirmado: pedir um código de 6 dígitos seria pedir o impossível. Ausente
|
|
23
|
+
(stores que não reportam o estado) mantém o campo visível. --}}
|
|
24
|
+
@if(!noEnrollment && totpAvailable !== false)
|
|
22
25
|
<div class="mt-6">
|
|
23
26
|
<label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('mfa_challenge.code_label') }}</label>
|
|
24
27
|
<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code"
|
|
@@ -56,7 +59,9 @@
|
|
|
56
59
|
<p id="passkey-error" class="mt-3 hidden text-sm text-red-600">{{ t('mfa_challenge.passkey_error') }}</p>
|
|
57
60
|
@end
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
{{-- Códigos de recuperação só existem junto com o TOTP (são gerados no
|
|
63
|
+
enrollment); sem ele, a seção não tem o que receber. --}}
|
|
64
|
+
@if(!noEnrollment && totpAvailable !== false)
|
|
60
65
|
<details class="mt-6 text-sm text-gray-600">
|
|
61
66
|
<summary class="cursor-pointer hover:underline">{{ t('mfa_challenge.recovery_summary') }}</summary>
|
|
62
67
|
<form method="POST" action="/auth/interaction/{{ uid }}/mfa" class="mt-3">
|
|
@@ -250,10 +250,18 @@ export interface MfaCapability {
|
|
|
250
250
|
* mecanismo de "trusted devices": um cookie de confiança emitido ANTES desse
|
|
251
251
|
* instante é considerado inválido (re-enrolar MFA revoga a confiança). Pode ser
|
|
252
252
|
* `null`/ausente quando o MFA não está ativo ou o store não rastreia o instante.
|
|
253
|
+
*
|
|
254
|
+
* `totp` diz se há um app autenticador CONFIRMADO (segredo + códigos de
|
|
255
|
+
* recuperação). É diferente de `enabled`: registrar uma passkey também liga o
|
|
256
|
+
* MFA, e remover a última passkey não o desliga — então `enabled` sozinho não
|
|
257
|
+
* responde "existe um segundo fator que esta pessoa consegue apresentar?".
|
|
258
|
+
* Quem decide mostrar (ou não) o desafio precisa de `totp`; stores antigos que
|
|
259
|
+
* o omitem continuam funcionando, só não distinguem os dois casos.
|
|
253
260
|
*/
|
|
254
261
|
getMfaState(accountId: string): Promise<{
|
|
255
262
|
enabled: boolean;
|
|
256
263
|
enabledAt?: number | null;
|
|
264
|
+
totp?: boolean;
|
|
257
265
|
}>;
|
|
258
266
|
/**
|
|
259
267
|
* Inicia o enrollment TOTP: gera um segredo PENDENTE (mfaEnabledAt continua
|
|
@@ -25,7 +25,15 @@ export function buildMfa(ctx) {
|
|
|
25
25
|
const state = await repo.read(accountId);
|
|
26
26
|
// `enabledAt` (epoch ms) habilita o trusted-device check: um cookie de
|
|
27
27
|
// confiança emitido ANTES deste instante é inválido (re-enrolar revoga).
|
|
28
|
-
|
|
28
|
+
// `totp` é o fator REALMENTE utilizável: segredo confirmado + códigos de
|
|
29
|
+
// recuperação gravados. `enabled` também liga ao registrar passkey (ver
|
|
30
|
+
// `webauthn.ts`) e não desliga ao remover a última — sozinho, não diz se
|
|
31
|
+
// sobrou algum fator para desafiar.
|
|
32
|
+
return {
|
|
33
|
+
enabled: !!state?.mfaEnabledAt,
|
|
34
|
+
enabledAt: state?.mfaEnabledAt ?? null,
|
|
35
|
+
totp: !!(state?.mfaEnabledAt && state?.totpSecret && state?.recoveryCodes),
|
|
36
|
+
};
|
|
29
37
|
},
|
|
30
38
|
async startTotpEnrollment(accountId) {
|
|
31
39
|
// O email/QR vem do model principal; só o ESTADO de MFA vive em auth_mfa.
|
|
@@ -41,6 +41,34 @@ export default class AuthInteractionController {
|
|
|
41
41
|
* Retorna `undefined` quando não há step-up — completeLogin usa o default.
|
|
42
42
|
*/
|
|
43
43
|
private stepUpExtra;
|
|
44
|
+
/**
|
|
45
|
+
* Gate do segundo fator, compartilhado pelos caminhos de login: senha, link
|
|
46
|
+
* mágico, código por e-mail e troca forçada de senha. Antes disto só o login
|
|
47
|
+
* por senha passava pelo MFA — link e código completavam a interaction direto,
|
|
48
|
+
* então uma conta com TOTP ou passkey entrava apresentando só o e-mail e o
|
|
49
|
+
* segundo fator virava enfeite.
|
|
50
|
+
*
|
|
51
|
+
* A pergunta que o gate responde é "existe um fator que esta pessoa consegue
|
|
52
|
+
* apresentar AGORA?", e não `mfa.enabled` — que liga ao registrar uma passkey
|
|
53
|
+
* e não desliga ao remover a última. Fator utilizável = TOTP confirmado
|
|
54
|
+
* (`mfa.totp`) OU ao menos uma passkey registrada. Pela regra antiga, quem
|
|
55
|
+
* registrava uma passkey e depois a removia ficava preso num desafio TOTP que
|
|
56
|
+
* não tinha como responder.
|
|
57
|
+
*
|
|
58
|
+
* - sem fator e sem step-up → `none`, o login segue;
|
|
59
|
+
* - step-up (acr) exigindo MFA numa conta sem fator → `challenge` com a
|
|
60
|
+
* instrução de enrolar (não há o que desafiar);
|
|
61
|
+
* - fator presente, trusted-device válido e sem step-up → `trusted`; quem
|
|
62
|
+
* chamou decide como finalizar, porque o amr muda conforme o caminho;
|
|
63
|
+
* - caso geral → `challenge`, com o accountId em `MFA_PENDING_KEY`.
|
|
64
|
+
*/
|
|
65
|
+
private secondFactorGate;
|
|
66
|
+
/**
|
|
67
|
+
* true se a conta tem um app autenticador CONFIRMADO. Stores que não reportam
|
|
68
|
+
* `totp` (a chave é opcional em {@link MfaCapability}) caem no `enabled`, o
|
|
69
|
+
* comportamento antigo.
|
|
70
|
+
*/
|
|
71
|
+
private hasTotp;
|
|
44
72
|
/** true se o store suporta passkeys E a conta tem ao menos uma registrada. */
|
|
45
73
|
private hasPasskeys;
|
|
46
74
|
/**
|
|
@@ -405,55 +405,24 @@ export default class AuthInteractionController {
|
|
|
405
405
|
}
|
|
406
406
|
// Admin: prossegue normalmente (sem bloqueio).
|
|
407
407
|
}
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
error: translate(cfg.messages, 'mfa_challenge.required_no_enrollment'),
|
|
424
|
-
noEnrollment: true,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
// Trusted device: se o mecanismo está ligado, a conta JÁ tem MFA enrolado e
|
|
428
|
-
// o request NÃO é um step-up (que sempre força o MFA), um cookie de confiança
|
|
429
|
-
// válido para ESTA conta pula o 2º fator. amr fica `['pwd']` (sem acr de MFA).
|
|
430
|
-
if (cfg.trustedDevices.enabled && mfa.enabled && !mfaRequired) {
|
|
431
|
-
const trusted = await this.checkTrustedDevice(ctx, acc.id, mfa.enabledAt ?? null);
|
|
432
|
-
if (trusted) {
|
|
433
|
-
await service.interactions.completeLogin(ctx, acc.id, { amr: ['pwd'] });
|
|
434
|
-
await notifyLoginSuccess(ctx, cfg, {
|
|
435
|
-
accountId: acc.id,
|
|
436
|
-
email,
|
|
437
|
-
ip,
|
|
438
|
-
clientId,
|
|
439
|
-
trustedDevice: true,
|
|
440
|
-
});
|
|
441
|
-
forgetLoginEmail(ctx);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
ctx.session.put(MFA_PENDING_KEY, acc.id);
|
|
446
|
-
// Passkey disponível como alternativa ao TOTP se o store suporta E a conta
|
|
447
|
-
// tem ao menos uma credencial registrada.
|
|
448
|
-
const passkeyAvailable = await this.hasPasskeys(cfg, acc.id);
|
|
449
|
-
return render(ctx, 'mfa-challenge', {
|
|
450
|
-
uid: ctx.request.param('uid'),
|
|
451
|
-
csrfToken: ctx.request.csrfToken,
|
|
452
|
-
brand,
|
|
453
|
-
passkeyAvailable,
|
|
454
|
-
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
455
|
-
trustedDeviceDays: cfg.trustedDevices.days,
|
|
408
|
+
// Gate do 2º fator — a MESMA regra dos três caminhos de login (ver
|
|
409
|
+
// `secondFactorGate`). Quando desafia, não finaliza a interaction: guarda o
|
|
410
|
+
// accountId pendente e devolve a tela.
|
|
411
|
+
const gate = await this.secondFactorGate(ctx, cfg, acc.id, details);
|
|
412
|
+
if (gate.kind === 'challenge')
|
|
413
|
+
return gate.response;
|
|
414
|
+
if (gate.kind === 'trusted') {
|
|
415
|
+
// Dispositivo confiável: pula o 2º fator. amr fica `['pwd']` (sem acr de MFA).
|
|
416
|
+
await service.interactions.completeLogin(ctx, acc.id, { amr: ['pwd'] });
|
|
417
|
+
await notifyLoginSuccess(ctx, cfg, {
|
|
418
|
+
accountId: acc.id,
|
|
419
|
+
email,
|
|
420
|
+
ip,
|
|
421
|
+
clientId,
|
|
422
|
+
trustedDevice: true,
|
|
456
423
|
});
|
|
424
|
+
forgetLoginEmail(ctx);
|
|
425
|
+
return;
|
|
457
426
|
}
|
|
458
427
|
// Sem MFA: finaliza a interaction (escreve o 303 de volta para o client).
|
|
459
428
|
// Resolve session_policy para remember-me e single-session.
|
|
@@ -531,6 +500,7 @@ export default class AuthInteractionController {
|
|
|
531
500
|
error: translate(cfg.messages, 'errors.otp_locked'),
|
|
532
501
|
brand,
|
|
533
502
|
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
503
|
+
totpAvailable: await this.hasTotp(cfg, accountId),
|
|
534
504
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
535
505
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
536
506
|
otpLocked: true,
|
|
@@ -564,6 +534,7 @@ export default class AuthInteractionController {
|
|
|
564
534
|
error: translate(cfg.messages, 'errors.otp_locked'),
|
|
565
535
|
brand,
|
|
566
536
|
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
537
|
+
totpAvailable: await this.hasTotp(cfg, accountId),
|
|
567
538
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
568
539
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
569
540
|
otpLocked: true,
|
|
@@ -575,6 +546,7 @@ export default class AuthInteractionController {
|
|
|
575
546
|
error: translate(cfg.messages, 'errors.invalid_code'),
|
|
576
547
|
brand,
|
|
577
548
|
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
549
|
+
totpAvailable: await this.hasTotp(cfg, accountId),
|
|
578
550
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
579
551
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
580
552
|
});
|
|
@@ -619,6 +591,84 @@ export default class AuthInteractionController {
|
|
|
619
591
|
return undefined;
|
|
620
592
|
return { acr: cfg.stepUp.mfaAcr, amr: ['mfa', method] };
|
|
621
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Gate do segundo fator, compartilhado pelos caminhos de login: senha, link
|
|
596
|
+
* mágico, código por e-mail e troca forçada de senha. Antes disto só o login
|
|
597
|
+
* por senha passava pelo MFA — link e código completavam a interaction direto,
|
|
598
|
+
* então uma conta com TOTP ou passkey entrava apresentando só o e-mail e o
|
|
599
|
+
* segundo fator virava enfeite.
|
|
600
|
+
*
|
|
601
|
+
* A pergunta que o gate responde é "existe um fator que esta pessoa consegue
|
|
602
|
+
* apresentar AGORA?", e não `mfa.enabled` — que liga ao registrar uma passkey
|
|
603
|
+
* e não desliga ao remover a última. Fator utilizável = TOTP confirmado
|
|
604
|
+
* (`mfa.totp`) OU ao menos uma passkey registrada. Pela regra antiga, quem
|
|
605
|
+
* registrava uma passkey e depois a removia ficava preso num desafio TOTP que
|
|
606
|
+
* não tinha como responder.
|
|
607
|
+
*
|
|
608
|
+
* - sem fator e sem step-up → `none`, o login segue;
|
|
609
|
+
* - step-up (acr) exigindo MFA numa conta sem fator → `challenge` com a
|
|
610
|
+
* instrução de enrolar (não há o que desafiar);
|
|
611
|
+
* - fator presente, trusted-device válido e sem step-up → `trusted`; quem
|
|
612
|
+
* chamou decide como finalizar, porque o amr muda conforme o caminho;
|
|
613
|
+
* - caso geral → `challenge`, com o accountId em `MFA_PENDING_KEY`.
|
|
614
|
+
*/
|
|
615
|
+
async secondFactorGate(ctx, cfg, accountId, details) {
|
|
616
|
+
const mfaRequired = this.acrRequiresMfa(cfg, details);
|
|
617
|
+
const mfa = (await cfg.accountStore.getMfaState?.(accountId)) ?? { enabled: false };
|
|
618
|
+
// Passkey disponível como alternativa ao TOTP se o store suporta E a conta
|
|
619
|
+
// tem ao menos uma credencial registrada.
|
|
620
|
+
const passkeyAvailable = await this.hasPasskeys(cfg, accountId);
|
|
621
|
+
// Store que não reporta `totp` (a chave é opcional) cai no `enabled` — o
|
|
622
|
+
// comportamento antigo. Nunca o contrário: assumir "sem TOTP" por omissão
|
|
623
|
+
// deixaria de desafiar quem tem o fator.
|
|
624
|
+
const totpAvailable = mfa.totp ?? !!mfa.enabled;
|
|
625
|
+
const hasFactor = passkeyAvailable || totpAvailable;
|
|
626
|
+
if (!hasFactor && !mfaRequired)
|
|
627
|
+
return { kind: 'none' };
|
|
628
|
+
const render = cfg.render;
|
|
629
|
+
const brand = brandFor(cfg.branding, details.params.client_id, details.params.audience);
|
|
630
|
+
const uid = ctx.request.param('uid');
|
|
631
|
+
if (!hasFactor) {
|
|
632
|
+
return {
|
|
633
|
+
kind: 'challenge',
|
|
634
|
+
response: await render(ctx, 'mfa-challenge', {
|
|
635
|
+
uid,
|
|
636
|
+
csrfToken: ctx.request.csrfToken,
|
|
637
|
+
brand,
|
|
638
|
+
passkeyAvailable: false,
|
|
639
|
+
error: translate(cfg.messages, 'mfa_challenge.required_no_enrollment'),
|
|
640
|
+
noEnrollment: true,
|
|
641
|
+
}),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (cfg.trustedDevices.enabled && !mfaRequired) {
|
|
645
|
+
const trusted = await this.checkTrustedDevice(ctx, accountId, mfa.enabledAt ?? null);
|
|
646
|
+
if (trusted)
|
|
647
|
+
return { kind: 'trusted' };
|
|
648
|
+
}
|
|
649
|
+
ctx.session.put(MFA_PENDING_KEY, accountId);
|
|
650
|
+
return {
|
|
651
|
+
kind: 'challenge',
|
|
652
|
+
response: await render(ctx, 'mfa-challenge', {
|
|
653
|
+
uid,
|
|
654
|
+
csrfToken: ctx.request.csrfToken,
|
|
655
|
+
brand,
|
|
656
|
+
passkeyAvailable,
|
|
657
|
+
totpAvailable,
|
|
658
|
+
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
659
|
+
trustedDeviceDays: cfg.trustedDevices.days,
|
|
660
|
+
}),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* true se a conta tem um app autenticador CONFIRMADO. Stores que não reportam
|
|
665
|
+
* `totp` (a chave é opcional em {@link MfaCapability}) caem no `enabled`, o
|
|
666
|
+
* comportamento antigo.
|
|
667
|
+
*/
|
|
668
|
+
async hasTotp(cfg, accountId) {
|
|
669
|
+
const mfa = (await cfg.accountStore.getMfaState?.(accountId)) ?? { enabled: false };
|
|
670
|
+
return mfa.totp ?? !!mfa.enabled;
|
|
671
|
+
}
|
|
622
672
|
/** true se o store suporta passkeys E a conta tem ao menos uma registrada. */
|
|
623
673
|
async hasPasskeys(cfg, accountId) {
|
|
624
674
|
if (!supportsPasskeys(cfg.accountStore))
|
|
@@ -920,12 +970,21 @@ export default class AuthInteractionController {
|
|
|
920
970
|
error: translate(cfg.messages, accountStatusErrorKey(magicLinkStatusGate.reason)),
|
|
921
971
|
});
|
|
922
972
|
}
|
|
973
|
+
// Conta com segundo fator não termina o login só com o e-mail.
|
|
974
|
+
const magicGate = await this.secondFactorGate(ctx, cfg, acc.id, await service.interactions.details(ctx));
|
|
975
|
+
if (magicGate.kind === 'challenge')
|
|
976
|
+
return magicGate.response;
|
|
923
977
|
await notifyLoginSuccess(ctx, cfg, {
|
|
924
978
|
accountId: acc.id,
|
|
925
979
|
email: acc.email,
|
|
926
980
|
ip,
|
|
927
981
|
clientId: clientId ?? null,
|
|
928
982
|
metadata: { method: 'magic_link' },
|
|
983
|
+
// `trusted` = o gate já validou o cookie de confiança nesta request; dizer
|
|
984
|
+
// isso aqui poupa `notifyLoginSuccess` de reler o mesmo cookie para decidir
|
|
985
|
+
// se é dispositivo novo. O amr continua `['email']`: o fator primário foi o
|
|
986
|
+
// e-mail, a confiança só dispensou o segundo.
|
|
987
|
+
trustedDevice: magicGate.kind === 'trusted',
|
|
929
988
|
});
|
|
930
989
|
forgetLoginEmail(ctx);
|
|
931
990
|
await service.interactions.completeLogin(ctx, acc.id, { amr: ['email'] });
|
|
@@ -1033,12 +1092,17 @@ export default class AuthInteractionController {
|
|
|
1033
1092
|
ip,
|
|
1034
1093
|
clientId,
|
|
1035
1094
|
});
|
|
1095
|
+
// Conta com segundo fator não termina o login só com o código do e-mail.
|
|
1096
|
+
const otpGate = await this.secondFactorGate(ctx, cfg, result.account.id, await service.interactions.details(ctx));
|
|
1097
|
+
if (otpGate.kind === 'challenge')
|
|
1098
|
+
return otpGate.response;
|
|
1036
1099
|
await notifyLoginSuccess(ctx, cfg, {
|
|
1037
1100
|
accountId: result.account.id,
|
|
1038
1101
|
email: result.account.email,
|
|
1039
1102
|
ip,
|
|
1040
1103
|
clientId: clientId ?? null,
|
|
1041
1104
|
metadata: { method: 'otp' },
|
|
1105
|
+
trustedDevice: otpGate.kind === 'trusted',
|
|
1042
1106
|
});
|
|
1043
1107
|
forgetLoginEmail(ctx);
|
|
1044
1108
|
return service.interactions.completeLogin(ctx, result.account.id, { amr: ['email'] });
|
|
@@ -1225,6 +1289,7 @@ export default class AuthInteractionController {
|
|
|
1225
1289
|
error: translate(cfg.messages, 'mfa_challenge.passkey_error'),
|
|
1226
1290
|
brand,
|
|
1227
1291
|
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
1292
|
+
totpAvailable: await this.hasTotp(cfg, accountId),
|
|
1228
1293
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1229
1294
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1230
1295
|
});
|
|
@@ -1247,6 +1312,7 @@ export default class AuthInteractionController {
|
|
|
1247
1312
|
error: translate(cfg.messages, 'errors.email_unverified'),
|
|
1248
1313
|
brand,
|
|
1249
1314
|
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
1315
|
+
totpAvailable: await this.hasTotp(cfg, accountId),
|
|
1250
1316
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1251
1317
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1252
1318
|
});
|
|
@@ -1277,6 +1343,7 @@ export default class AuthInteractionController {
|
|
|
1277
1343
|
error: translate(cfg.messages, accountStatusErrorKey(passkeyStatusGate.reason)),
|
|
1278
1344
|
brand,
|
|
1279
1345
|
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
1346
|
+
totpAvailable: await this.hasTotp(cfg, accountId),
|
|
1280
1347
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1281
1348
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1282
1349
|
});
|
|
@@ -1373,20 +1440,10 @@ export default class AuthInteractionController {
|
|
|
1373
1440
|
});
|
|
1374
1441
|
// Senha trocada: limpa o step e finaliza o login.
|
|
1375
1442
|
ctx.session.forget(PASSWORD_EXPIRED_KEY);
|
|
1376
|
-
// Verifica se precisa de MFA mesmo após a troca.
|
|
1377
|
-
const
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
const passkeyAvailable = await this.hasPasskeys(cfg, accountId);
|
|
1381
|
-
return render(ctx, 'mfa-challenge', {
|
|
1382
|
-
uid: ctx.request.param('uid'),
|
|
1383
|
-
csrfToken: ctx.request.csrfToken,
|
|
1384
|
-
brand,
|
|
1385
|
-
passkeyAvailable,
|
|
1386
|
-
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1387
|
-
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1443
|
+
// Verifica se precisa de MFA mesmo após a troca — mesma regra do resto.
|
|
1444
|
+
const expiredGate = await this.secondFactorGate(ctx, cfg, accountId, details);
|
|
1445
|
+
if (expiredGate.kind === 'challenge')
|
|
1446
|
+
return expiredGate.response;
|
|
1390
1447
|
await service.interactions.completeLogin(ctx, accountId);
|
|
1391
1448
|
const account = await cfg.accountStore.findById(accountId);
|
|
1392
1449
|
await notifyLoginSuccess(ctx, cfg, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|