@adonis-agora/authkit-server 0.70.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.
@@ -537,26 +537,10 @@ export interface LoginConfigInput {
537
537
  * idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
538
538
  */
539
539
  otp?: OtpLoginConfigInput;
540
- /**
541
- * PONTE TEMPORÁRIA: no passo de identificador, quando o e-mail normalizado
542
- * (`trim` + `toLowerCase`) não acha conta nenhuma, tenta também o endereço
543
- * exatamente como foi digitado e a normalização LEGADA (os defaults do
544
- * validator.js que o cadastro aplicava até a v0.68 e que, no gmail, removiam
545
- * pontos e sub-endereço `+tag`). Só aceita quando essas formas apontam para
546
- * EXATAMENTE UMA conta — empate é tratado como "não achei".
547
- *
548
- * Existe para que as contas que nasceram com o endereço mutilado
549
- * (`davi.carvalho96@gmail.com` gravado como `davicarvalho96@gmail.com`)
550
- * continuem tendo caminho de volta. Default: `true` — desligar tranca essas
551
- * contas do lado de fora. Depois de migrar os endereços gravados, desligue e,
552
- * mais adiante, a ponte inteira sai da lib (ver `host/email_identifier.ts`).
553
- */
554
- legacyEmailFallback?: boolean;
555
540
  }
556
541
  export interface ResolvedLoginConfig {
557
542
  requireVerifiedEmail: boolean;
558
543
  otp: ResolvedOtpLoginConfig;
559
- legacyEmailFallback: boolean;
560
544
  }
561
545
  export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
562
546
  /**
@@ -137,7 +137,6 @@ export function resolveLogin(input) {
137
137
  return {
138
138
  requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
139
139
  otp: resolveOtpLoginConfig(input?.otp),
140
- legacyEmailFallback: input?.legacyEmailFallback ?? true,
141
140
  };
142
141
  }
143
142
  export function resolveRegistration(input) {
@@ -4,7 +4,7 @@ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../../pagination.js';
4
4
  import { PasswordPolicyError } from '../../password/password_manager.js';
5
5
  import { AccountDeletionService } from '../account_deletion_service.js';
6
6
  import { sendPasswordResetEmail } from '../default_mailer.js';
7
- import { normalizeEmailIdentifier, resolveEmailIdentifier } from '../email_identifier.js';
7
+ import { normalizeEmailIdentifier } from '../email_identifier.js';
8
8
  import { authkitOrigin } from '../origin.js';
9
9
  import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
10
10
  /**
@@ -29,12 +29,10 @@ export class AdminUsersService {
29
29
  // serviço também é chamado direto (console/API/host) — normaliza aqui para
30
30
  // que nenhum caminho grave um endereço que o login não encontra.
31
31
  const email = normalizeEmailIdentifier(input.email);
32
- // Duplicado: enxerga também a conta gravada com o endereço mutilado pelo
33
- // cadastro antigo (ponte legada) — senão o admin criaria uma SEGUNDA conta
34
- // para quem o cadastro público recusaria com `email_taken`.
35
- const existing = (await resolveEmailIdentifier(store, input.email, {
36
- legacyFallback: this.cfg.login?.legacyEmailFallback ?? true,
37
- })).account;
32
+ // Duplicado pela MESMA forma que o login busca — a normalizada. Contas
33
+ // gravadas com outra grafia (import/convite antigos) só entram nesta conta
34
+ // depois do `authkit:users:normalize-emails`.
35
+ const existing = await store.findByEmail(email);
38
36
  if (existing)
39
37
  return { ok: false, reason: 'email_taken' };
40
38
  const hasPassword = !!input.password;
@@ -3,7 +3,7 @@ import { accountHome } from '../account_home.js';
3
3
  import { getAccountLoginUrl } from '../account_login_url.js';
4
4
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
5
5
  import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
6
- import { resolveEmailIdentifier } from '../email_identifier.js';
6
+ import { normalizeEmailIdentifier } from '../email_identifier.js';
7
7
  import { translate } from '../i18n.js';
8
8
  import { endBridgedIdpSession } from '../idp_session_bridge.js';
9
9
  import { attemptPasswordLogin } from '../login_attempt.js';
@@ -68,18 +68,13 @@ export default class AccountSessionController {
68
68
  // Verificação + lockout + auditoria de falha centralizados (sem clientId no console).
69
69
  // M1: passa `settings` p/ o lockout (e verified-email/expiração) runtime valerem aqui também.
70
70
  const settings = await resolveRuntimeSettings(ctx);
71
- // L6: `resolveEmailIdentifier` normaliza (trim + lowercase, a MESMA função do
72
- // cadastro e do passo de identificador do login OIDC) e devolve o endereço sob
73
- // o qual a conta está gravada — de modo que o lookup, o lockout (keyed por
71
+ // L6: normaliza (trim + lowercase, a MESMA função do cadastro e do passo de
72
+ // identificador do login OIDC) para que o lookup, o lockout (keyed por
74
73
  // e-mail) e a auditoria usem a forma canônica, independente do casing/espaços
75
- // digitados. A ponte legada faz a conta que nasceu com o endereço mutilado
76
- // entrar no console pelo endereço REAL, como no login OIDC. A tela de erro é a
77
- // mesma (credencial inválida), ache conta ou não.
78
- const { lookupEmail } = await resolveEmailIdentifier(cfg.accountStore, rawEmail, {
79
- legacyFallback: cfg.login?.legacyEmailFallback ?? true,
80
- });
74
+ // digitados. A tela de erro é a mesma (credencial inválida), ache conta ou não.
75
+ const email = normalizeEmailIdentifier(rawEmail);
81
76
  const result = await attemptPasswordLogin(cfg, {
82
- email: lookupEmail,
77
+ email,
83
78
  password,
84
79
  ip,
85
80
  logger: ctx.logger,
@@ -12,11 +12,11 @@ export default class AuthInteractionController {
12
12
  * `trim` + `toLowerCase`) — antes disto o valor cru ia para a sessão e a busca
13
13
  * no store era por igualdade exata, então nem `Davi@x.com` achava `davi@x.com`.
14
14
  *
15
- * A resolução acontece AQUI (uma vez, onde o valor digitado ainda existe) e
16
- * não a cada request do passo 2. Guarda DUAS coisas: o digitado (o que a tela
17
- * mostra) e, se a ponte de compatibilidade precisou de outra forma, o endereço
18
- * sob o qual a conta está gravada (o que vai para o store). A resposta continua
19
- * sendo o mesmo redirect incondicional, ache conta ou não.
15
+ * À PROVA DE ENUMERAÇÃO: o passo NÃO toca no account store — só normaliza e
16
+ * guarda. A resposta é o MESMO redirect incondicional, exista a conta ou não,
17
+ * e não há busca alguma aqui para diferenciar os dois casos nem no tempo de
18
+ * resposta. Quem decide se a conta existe é o passo 2, que renderiza a mesma
19
+ * tela nos dois casos.
20
20
  */
21
21
  identifier(ctx: HttpContext): Promise<void>;
22
22
  /**
@@ -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
  /**