@adonis-agora/authkit-server 0.54.0 → 0.55.1
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/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/providers/authkit_server_provider.js +18 -0
- package/build/src/accounts/lucid_account_store.d.ts +35 -0
- package/build/src/accounts/lucid_account_store.js +9 -0
- package/build/src/accounts/lucid_store/core.js +88 -19
- package/build/src/accounts/lucid_store/shared.d.ts +14 -0
- package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
- package/build/src/accounts/lucid_store/token_hash.js +145 -0
- package/build/src/define_config.d.ts +93 -8
- package/build/src/define_config.js +28 -2
- package/build/src/host/account_api/account_api_controller.js +5 -4
- package/build/src/host/admin_api/admin_users_service.js +2 -1
- package/build/src/host/admin_api/api_orgs_controller.js +2 -1
- package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
- package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
- package/build/src/host/admin_console/console_orgs_controller.js +3 -1
- package/build/src/host/admin_validators.d.ts +3 -3
- package/build/src/host/auth_host_config.d.ts +30 -0
- package/build/src/host/auth_host_config.js +15 -0
- package/build/src/host/config_locks.d.ts +29 -0
- package/build/src/host/config_locks.js +46 -0
- package/build/src/host/console_session.d.ts +38 -2
- package/build/src/host/console_session.js +46 -2
- package/build/src/host/controllers/account_mfa_controller.js +5 -5
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +6 -5
- package/build/src/host/controllers/interaction_controller.js +122 -26
- package/build/src/host/controllers/registration_controller.js +5 -4
- package/build/src/host/controllers/social_controller.js +37 -0
- package/build/src/host/default_mailer.d.ts +36 -5
- package/build/src/host/default_mailer.js +63 -10
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/login_attempt.d.ts +88 -2
- package/build/src/host/login_attempt.js +101 -48
- package/build/src/host/login_notify.js +2 -2
- package/build/src/host/oidc_rp_guard.d.ts +25 -2
- package/build/src/host/oidc_rp_guard.js +55 -9
- package/build/src/host/origin.d.ts +21 -0
- package/build/src/host/origin.js +22 -0
- package/build/src/host/register_auth_host.d.ts +104 -3
- package/build/src/host/register_auth_host.js +222 -26
- package/build/src/host/runtime_settings.d.ts +16 -0
- package/build/src/host/runtime_settings.js +24 -0
- package/build/src/host/runtime_toggles.d.ts +10 -0
- package/build/src/host/runtime_toggles.js +4 -0
- package/build/src/host/security_notice_service.d.ts +4 -2
- package/build/src/host/security_notice_service.js +4 -2
- package/build/src/host/sudo/index.d.ts +8 -0
- package/build/src/host/sudo/index.js +8 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
- package/build/src/host/sudo/methods/magic_link.js +37 -13
- package/build/src/host/sudo/runtime.d.ts +44 -4
- package/build/src/host/sudo/runtime.js +90 -6
- package/build/src/host/sudo/satisfiability.d.ts +62 -0
- package/build/src/host/sudo/satisfiability.js +89 -0
- package/build/src/password/common_passwords.js +27 -7
- package/build/src/provider/oidc_service.js +30 -13
- package/build/src/provider/token_exchange.d.ts +25 -1
- package/build/src/provider/token_exchange.js +29 -2
- package/package.json +6 -3
- /package/build/{password → src/password}/common_passwords.txt +0 -0
|
@@ -9,6 +9,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { accountPath } from './account_paths.js';
|
|
10
10
|
import { renderTransactionalEmail } from './email_templates.js';
|
|
11
11
|
import { resolveMessages, translate } from './i18n.js';
|
|
12
|
+
import { authkitOrigin } from './origin.js';
|
|
12
13
|
let mailServicePromise;
|
|
13
14
|
/**
|
|
14
15
|
* Importa o service de mail do HOST de forma preguiçosa e fail-safe.
|
|
@@ -174,11 +175,11 @@ export async function sendEmailChangeConfirmationEmail(ctx, data) {
|
|
|
174
175
|
* Envia o e-mail de alerta de NOVO acesso à conta (login de um IP novo).
|
|
175
176
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
176
177
|
*/
|
|
177
|
-
export async function sendNewLoginEmail(ctx, data) {
|
|
178
|
+
export async function sendNewLoginEmail(ctx, cfg, data) {
|
|
178
179
|
try {
|
|
179
180
|
const brand = resolveBrand(ctx);
|
|
180
181
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
181
|
-
const origin =
|
|
182
|
+
const origin = authkitOrigin(cfg);
|
|
182
183
|
const intro = [
|
|
183
184
|
translate(t, 'mail.new_login.intro'),
|
|
184
185
|
translate(t, 'mail.new_login.when', { date: data.when }),
|
|
@@ -208,11 +209,11 @@ export async function sendNewLoginEmail(ctx, data) {
|
|
|
208
209
|
* Envia o e-mail de alerta de NOVO DISPOSITIVO (login sem cookie de dispositivo
|
|
209
210
|
* confiável). Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
210
211
|
*/
|
|
211
|
-
export async function sendNewDeviceLoginEmail(ctx, data) {
|
|
212
|
+
export async function sendNewDeviceLoginEmail(ctx, cfg, data) {
|
|
212
213
|
try {
|
|
213
214
|
const brand = resolveBrand(ctx);
|
|
214
215
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
215
|
-
const origin =
|
|
216
|
+
const origin = authkitOrigin(cfg);
|
|
216
217
|
const lines = [
|
|
217
218
|
translate(t, 'mail.new_login.intro'),
|
|
218
219
|
translate(t, 'mail.new_login.when', { date: data.when }),
|
|
@@ -293,16 +294,68 @@ export async function sendMagicLinkEmail(ctx, data) {
|
|
|
293
294
|
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar magic link de login');
|
|
294
295
|
}
|
|
295
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
|
|
299
|
+
*
|
|
300
|
+
* POR QUE EXISTE. `sudoMethods.magicLink()` dependia EXCLUSIVAMENTE do hook
|
|
301
|
+
* `mail.onSudoLink`, e sem ele o método se declarava indisponível. Num host
|
|
302
|
+
* passwordless isso fechava o deadlock: sem senha, sem passkey e sem hook, a
|
|
303
|
+
* tela `/account/confirm` não tinha um único método satisfazível — e o usuário
|
|
304
|
+
* ficava trancado fora de exportar/excluir os próprios dados, do MFA, dos PATs
|
|
305
|
+
* e da troca de e-mail, inclusive do cadastro de passkey que destravaria tudo.
|
|
306
|
+
*
|
|
307
|
+
* A postura aqui é a MESMA de todos os outros e-mails da lib (reset de senha,
|
|
308
|
+
* verificação, magic link de login, avisos de segurança): o hook do host, quando
|
|
309
|
+
* existe, tem prioridade; sem hook, o próprio host-kit envia pelo mailer default
|
|
310
|
+
* (`@adonisjs/mail`) com branding e i18n; sem mailer, loga o link (dev).
|
|
311
|
+
*
|
|
312
|
+
* O TOKEN continua sendo o de sudo — emitido e verificado em
|
|
313
|
+
* `sudo/methods/magic_link.ts`, hasheado na sessão que pediu, de uso único,
|
|
314
|
+
* 5 min, vinculado ao `accountId` emissor. Isto aqui é só ENTREGA; nada é
|
|
315
|
+
* compartilhado com o token de login.
|
|
316
|
+
*
|
|
317
|
+
* @returns `true` quando o e-mail foi entregue ao mailer (ou logado no fallback
|
|
318
|
+
* de dev) e `false` quando o envio falhou de verdade. O chamador PRECISA disso:
|
|
319
|
+
* ao contrário dos outros e-mails da lib, um link de sudo que não saiu tem de
|
|
320
|
+
* apagar o pendente da sessão e recusar — flashar "link enviado" deixaria o
|
|
321
|
+
* usuário esperando um e-mail que nunca vem, na tela que já é o gargalo dele.
|
|
322
|
+
*/
|
|
323
|
+
export async function sendSudoLinkEmail(ctx, data) {
|
|
324
|
+
try {
|
|
325
|
+
const brand = resolveBrand(ctx);
|
|
326
|
+
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
327
|
+
const content = renderTransactionalEmail({
|
|
328
|
+
brand,
|
|
329
|
+
locale,
|
|
330
|
+
linkFallback: translate(t, 'mail.common.link_fallback'),
|
|
331
|
+
subject: translate(t, 'mail.sudo_link.subject'),
|
|
332
|
+
heading: translate(t, 'mail.sudo_link.heading'),
|
|
333
|
+
intro: translate(t, 'mail.sudo_link.intro'),
|
|
334
|
+
ctaLabel: translate(t, 'mail.sudo_link.cta'),
|
|
335
|
+
ctaUrl: data.sudoUrl,
|
|
336
|
+
footnote: translate(t, 'mail.sudo_link.fallback'),
|
|
337
|
+
});
|
|
338
|
+
const sent = await sendEmail(ctx, data.email, content);
|
|
339
|
+
if (!sent) {
|
|
340
|
+
ctx.logger?.info({ sudoUrl: data.sudoUrl, email: data.email }, 'authkit: link de confirmação de identidade (dev — @adonisjs/mail ausente)');
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
ctx.logger?.error({ err: error, email: data.email }, 'authkit: falha ao enviar link de confirmação de identidade');
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
296
349
|
/**
|
|
297
350
|
* Envia o e-mail de aviso de segurança ao e-mail ATUAL quando uma troca de
|
|
298
351
|
* e-mail é solicitada ("Alguém pediu troca para X — se não foi você...").
|
|
299
352
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
300
353
|
*/
|
|
301
|
-
export async function sendEmailChangeNoticeEmail(ctx, data) {
|
|
354
|
+
export async function sendEmailChangeNoticeEmail(ctx, cfg, data) {
|
|
302
355
|
try {
|
|
303
356
|
const brand = resolveBrand(ctx);
|
|
304
357
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
305
|
-
const origin =
|
|
358
|
+
const origin = authkitOrigin(cfg);
|
|
306
359
|
const content = renderTransactionalEmail({
|
|
307
360
|
brand,
|
|
308
361
|
locale,
|
|
@@ -328,11 +381,11 @@ export async function sendEmailChangeNoticeEmail(ctx, data) {
|
|
|
328
381
|
* troca foi concluída ("Seu e-mail foi alterado de A para B — se não foi você...").
|
|
329
382
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
330
383
|
*/
|
|
331
|
-
export async function sendEmailChangedCompletedEmail(ctx, data) {
|
|
384
|
+
export async function sendEmailChangedCompletedEmail(ctx, cfg, data) {
|
|
332
385
|
try {
|
|
333
386
|
const brand = resolveBrand(ctx);
|
|
334
387
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
335
|
-
const origin =
|
|
388
|
+
const origin = authkitOrigin(cfg);
|
|
336
389
|
const content = renderTransactionalEmail({
|
|
337
390
|
brand,
|
|
338
391
|
locale,
|
|
@@ -361,11 +414,11 @@ export async function sendEmailChangedCompletedEmail(ctx, data) {
|
|
|
361
414
|
* MFA habilitado/desabilitado, passkey adicionada/removida, e-mail alterado).
|
|
362
415
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
363
416
|
*/
|
|
364
|
-
export async function sendSecurityNoticeEmail(ctx, data) {
|
|
417
|
+
export async function sendSecurityNoticeEmail(ctx, cfg, data) {
|
|
365
418
|
try {
|
|
366
419
|
const brand = resolveBrand(ctx);
|
|
367
420
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
368
|
-
const origin =
|
|
421
|
+
const origin = authkitOrigin(cfg);
|
|
369
422
|
const kindKey = `mail.security_notice.kind_${data.kind}`;
|
|
370
423
|
const kindLabel = translate(t, kindKey);
|
|
371
424
|
const intro = [
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -530,6 +530,11 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
530
530
|
'mail.magic_link.code_subject': string;
|
|
531
531
|
'mail.magic_link.code_intro': string;
|
|
532
532
|
'mail.magic_link.code_only_label': string;
|
|
533
|
+
'mail.sudo_link.subject': string;
|
|
534
|
+
'mail.sudo_link.heading': string;
|
|
535
|
+
'mail.sudo_link.intro': string;
|
|
536
|
+
'mail.sudo_link.cta': string;
|
|
537
|
+
'mail.sudo_link.fallback': string;
|
|
533
538
|
'mail.new_login.subject': string;
|
|
534
539
|
'mail.new_login.heading': string;
|
|
535
540
|
'mail.new_login.intro': string;
|
|
@@ -1243,6 +1248,11 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1243
1248
|
'mail.magic_link.code_subject': string;
|
|
1244
1249
|
'mail.magic_link.code_intro': string;
|
|
1245
1250
|
'mail.magic_link.code_only_label': string;
|
|
1251
|
+
'mail.sudo_link.subject': string;
|
|
1252
|
+
'mail.sudo_link.heading': string;
|
|
1253
|
+
'mail.sudo_link.intro': string;
|
|
1254
|
+
'mail.sudo_link.cta': string;
|
|
1255
|
+
'mail.sudo_link.fallback': string;
|
|
1246
1256
|
'mail.new_login.subject': string;
|
|
1247
1257
|
'mail.new_login.heading': string;
|
|
1248
1258
|
'mail.new_login.intro': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -573,6 +573,16 @@ export const DEFAULT_MESSAGES = {
|
|
|
573
573
|
'mail.magic_link.code_subject': 'Your login code',
|
|
574
574
|
'mail.magic_link.code_intro': 'Use the code below to sign in. It expires shortly and can be used once.',
|
|
575
575
|
'mail.magic_link.code_only_label': 'Enter this code to sign in:',
|
|
576
|
+
// Link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Copy DISTINTA da do magic link de
|
|
577
|
+
// login de propósito: este link não autentica ninguém — quem o abre já está
|
|
578
|
+
// logado e só reprova que é quem diz ser. Prometer "entre na sua conta" num
|
|
579
|
+
// e-mail que não faz login é o tipo de erro que ensina o usuário a clicar em
|
|
580
|
+
// qualquer link de "acesso" que chegue.
|
|
581
|
+
'mail.sudo_link.subject': 'Confirm it is you',
|
|
582
|
+
'mail.sudo_link.heading': 'Confirm your identity',
|
|
583
|
+
'mail.sudo_link.intro': 'You asked to confirm your identity before a sensitive account action. Click the button below to continue. The link expires in 5 minutes, can be used once and only works in the browser that requested it.',
|
|
584
|
+
'mail.sudo_link.cta': 'Confirm identity',
|
|
585
|
+
'mail.sudo_link.fallback': 'If you did not request this, you can ignore this email — nothing was changed.',
|
|
576
586
|
'mail.new_login.subject': 'New login to your account',
|
|
577
587
|
'mail.new_login.heading': 'New login detected',
|
|
578
588
|
'mail.new_login.intro': 'We detected a new login to your account.',
|
|
@@ -1365,6 +1375,12 @@ export const PT_BR_MESSAGES = {
|
|
|
1365
1375
|
'mail.magic_link.code_subject': 'Seu código de login',
|
|
1366
1376
|
'mail.magic_link.code_intro': 'Use o código abaixo para entrar. Ele expira em instantes e serve para um único acesso.',
|
|
1367
1377
|
'mail.magic_link.code_only_label': 'Digite este código para entrar:',
|
|
1378
|
+
// Link de CONFIRMAÇÃO DE IDENTIDADE (sudo) — ver o comentário no catálogo `en`.
|
|
1379
|
+
'mail.sudo_link.subject': 'Confirme que é você',
|
|
1380
|
+
'mail.sudo_link.heading': 'Confirme sua identidade',
|
|
1381
|
+
'mail.sudo_link.intro': 'Você pediu para confirmar sua identidade antes de uma ação sensível da conta. Clique no botão abaixo para continuar. O link expira em 5 minutos, serve uma única vez e só funciona no navegador que o pediu.',
|
|
1382
|
+
'mail.sudo_link.cta': 'Confirmar identidade',
|
|
1383
|
+
'mail.sudo_link.fallback': 'Se você não solicitou isso, pode ignorar este e-mail — nada mudou.',
|
|
1368
1384
|
'mail.new_login.subject': 'Novo login na sua conta',
|
|
1369
1385
|
'mail.new_login.heading': 'Novo login detectado',
|
|
1370
1386
|
'mail.new_login.intro': 'Detectamos um novo login na sua conta.',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthAccount } from '../accounts/account_store.js';
|
|
1
|
+
import type { AccountStore, AuthAccount } from '../accounts/account_store.js';
|
|
2
2
|
import type { AuditSink } from '../audit/audit_sink.js';
|
|
3
3
|
import type { ResolvedServerConfig } from '../define_config.js';
|
|
4
4
|
import type { SettingsCapability } from './runtime_settings.js';
|
|
@@ -23,7 +23,7 @@ export declare function isEmailUnverifiedBlock(cfg: ResolvedServerConfig, accoun
|
|
|
23
23
|
*
|
|
24
24
|
* @returns true se a senha expirou e deve ser trocada antes de completar o login.
|
|
25
25
|
*/
|
|
26
|
-
export declare function isPasswordExpired(cfg: ResolvedServerConfig, accountId: string, settings: SettingsCapability): Promise<boolean>;
|
|
26
|
+
export declare function isPasswordExpired(cfg: Pick<ResolvedServerConfig, 'accountStore'>, accountId: string, settings: SettingsCapability): Promise<boolean>;
|
|
27
27
|
/**
|
|
28
28
|
* Verifica se a conta está expirada por inatividade (account_expiration setting).
|
|
29
29
|
*
|
|
@@ -43,6 +43,92 @@ export declare function isAccountExpired(audit: AuditSink | null | undefined, ac
|
|
|
43
43
|
export interface LoginAttemptLogger {
|
|
44
44
|
warn(obj: unknown, msg?: string): void;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Motivo pelo qual um login foi recusado no gate de status de conta,
|
|
48
|
+
* compartilhado por TODOS os fluxos de login (senha, magic link, OTP,
|
|
49
|
+
* passkey, social, token-exchange/impersonation) — não só o de senha.
|
|
50
|
+
*/
|
|
51
|
+
export type AccountStatusReason = 'disabled' | 'password_expired' | 'account_expired';
|
|
52
|
+
/** Resultado do gate de status de conta. Discriminado por `allowed`. */
|
|
53
|
+
export type LoginAllowedResult = {
|
|
54
|
+
allowed: true;
|
|
55
|
+
} | {
|
|
56
|
+
allowed: false;
|
|
57
|
+
reason: AccountStatusReason;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Subconjunto de {@link ResolvedServerConfig} que o gate de status precisa: o
|
|
61
|
+
* accountStore (para as capacidades opcionais) e o audit sink (para os
|
|
62
|
+
* `login.failure`/`password.expired_change_forced`/`account.expired_login_blocked`
|
|
63
|
+
* emitidos). Estrutural — qualquer `ResolvedServerConfig` serve aqui sem cast;
|
|
64
|
+
* chamadores fora do host (ex.: `token_exchange.ts`, que não tem o
|
|
65
|
+
* `ResolvedServerConfig` inteiro em mãos) podem montar só este pedaço.
|
|
66
|
+
*/
|
|
67
|
+
export interface LoginAllowedConfig {
|
|
68
|
+
accountStore: AccountStore;
|
|
69
|
+
audit?: AuditSink;
|
|
70
|
+
}
|
|
71
|
+
/** Entrada comum aos dois gates de status (`assertAccountEnabled`/`assertAccountNotExpired`). */
|
|
72
|
+
export interface LoginAllowedInput {
|
|
73
|
+
email: string;
|
|
74
|
+
ip: string | null;
|
|
75
|
+
/** `clientId` só entra no evento de auditoria quando o fluxo o fornece (mesma regra do resto do arquivo). */
|
|
76
|
+
clientId?: string | null;
|
|
77
|
+
/** Ausente → password-expiry/account-expiry ficam no-op (precisam da runtime setting). */
|
|
78
|
+
settings?: SettingsCapability;
|
|
79
|
+
/**
|
|
80
|
+
* `true` → o chamador declara ser um fluxo SEM senha, e a checagem de senha
|
|
81
|
+
* vencida (`password_expiration`) é pulada. A de conta expirada por
|
|
82
|
+
* inatividade (`account_expiration`) continua valendo normalmente.
|
|
83
|
+
*
|
|
84
|
+
* Existe porque a expiração de senha pressupõe duas coisas: que a conta TEM
|
|
85
|
+
* senha, e que o fluxo tem um passo de troca obrigatória para onde mandar o
|
|
86
|
+
* usuário. `isPasswordExpired` trata `password_changed_at` NULL como vencida
|
|
87
|
+
* (seguro no fluxo de senha, que força a troca na 1ª vez) — num fluxo que não
|
|
88
|
+
* tem nenhuma das duas, isso recusa contas que nunca tiveram senha e não lhes
|
|
89
|
+
* oferece saída alguma.
|
|
90
|
+
*
|
|
91
|
+
* Default (ausente/`false`) = a checagem se aplica.
|
|
92
|
+
*/
|
|
93
|
+
passwordless?: boolean;
|
|
94
|
+
logger?: LoginAttemptLogger;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gate 1/2: conta desabilitada. Extraído de {@link attemptPasswordLogin}
|
|
98
|
+
* (bloco `isDisabled` original) para ser reutilizável por TODO fluxo que
|
|
99
|
+
* finaliza um login (`completeLogin`), não só o de senha.
|
|
100
|
+
*
|
|
101
|
+
* Capability-probed via `supportsAccountStatus` — store sem a capacidade
|
|
102
|
+
* degrada para "allowed" (comportamento preservado exatamente).
|
|
103
|
+
*/
|
|
104
|
+
export declare function assertAccountEnabled(cfg: LoginAllowedConfig, accountId: string, input: LoginAllowedInput): Promise<LoginAllowedResult>;
|
|
105
|
+
/**
|
|
106
|
+
* Gate 2/2: senha vencida (password_expiration) e conta expirada por
|
|
107
|
+
* inatividade (account_expiration). Extraído dos blocos originais de
|
|
108
|
+
* {@link attemptPasswordLogin} que seguiam o gate de e-mail não verificado.
|
|
109
|
+
*
|
|
110
|
+
* Capability/setting-probed: sem `input.settings`, ambas as checagens são
|
|
111
|
+
* no-op (mesma guarda `if (input.settings)` do código original).
|
|
112
|
+
*
|
|
113
|
+
* `input.passwordless === true` pula APENAS a checagem de senha vencida — ver
|
|
114
|
+
* {@link LoginAllowedInput.passwordless}. A expiração por inatividade continua.
|
|
115
|
+
*/
|
|
116
|
+
export declare function assertAccountNotExpired(cfg: LoginAllowedConfig, accountId: string, input: LoginAllowedInput): Promise<LoginAllowedResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Gate combinado (disabled → password_expired → account_expired), na ordem
|
|
119
|
+
* canônica. É o que os fluxos passwordless (magic link, OTP, passkey, social)
|
|
120
|
+
* e o token-exchange (impersonation) chamam UMA VEZ, imediatamente antes de
|
|
121
|
+
* `completeLogin` — eles não têm o gate de e-mail-não-verificado intercalado
|
|
122
|
+
* entre o disabled-check e os expiry-checks como `attemptPasswordLogin` tem,
|
|
123
|
+
* então uma chamada única e sequencial é observacionalmente idêntica a chamar
|
|
124
|
+
* os dois gates separados nessa ordem.
|
|
125
|
+
*
|
|
126
|
+
* `attemptPasswordLogin` NÃO usa este combinado — chama `assertAccountEnabled`
|
|
127
|
+
* e `assertAccountNotExpired` separadamente, com o gate de e-mail não
|
|
128
|
+
* verificado (`isEmailUnverifiedBlock`) intercalado entre os dois, para
|
|
129
|
+
* preservar a ordem de checagens original byte-a-byte.
|
|
130
|
+
*/
|
|
131
|
+
export declare function assertLoginAllowed(cfg: LoginAllowedConfig, accountId: string, input: LoginAllowedInput): Promise<LoginAllowedResult>;
|
|
46
132
|
/** Entrada de uma tentativa de login por senha (keyed por email). */
|
|
47
133
|
export interface PasswordLoginInput {
|
|
48
134
|
email: string;
|
|
@@ -124,6 +124,83 @@ export async function isAccountExpired(audit, accountId, settings, logger) {
|
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Gate 1/2: conta desabilitada. Extraído de {@link attemptPasswordLogin}
|
|
129
|
+
* (bloco `isDisabled` original) para ser reutilizável por TODO fluxo que
|
|
130
|
+
* finaliza um login (`completeLogin`), não só o de senha.
|
|
131
|
+
*
|
|
132
|
+
* Capability-probed via `supportsAccountStatus` — store sem a capacidade
|
|
133
|
+
* degrada para "allowed" (comportamento preservado exatamente).
|
|
134
|
+
*/
|
|
135
|
+
export async function assertAccountEnabled(cfg, accountId, input) {
|
|
136
|
+
const { email, ip, clientId } = input;
|
|
137
|
+
if (supportsAccountStatus(cfg.accountStore) && (await cfg.accountStore.isDisabled(accountId))) {
|
|
138
|
+
await cfg.audit?.record(clientId !== undefined
|
|
139
|
+
? {
|
|
140
|
+
type: 'login.failure',
|
|
141
|
+
email,
|
|
142
|
+
ip,
|
|
143
|
+
clientId,
|
|
144
|
+
metadata: { reason: 'disabled' },
|
|
145
|
+
}
|
|
146
|
+
: { type: 'login.failure', email, ip, metadata: { reason: 'disabled' } });
|
|
147
|
+
return { allowed: false, reason: 'disabled' };
|
|
148
|
+
}
|
|
149
|
+
return { allowed: true };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Gate 2/2: senha vencida (password_expiration) e conta expirada por
|
|
153
|
+
* inatividade (account_expiration). Extraído dos blocos originais de
|
|
154
|
+
* {@link attemptPasswordLogin} que seguiam o gate de e-mail não verificado.
|
|
155
|
+
*
|
|
156
|
+
* Capability/setting-probed: sem `input.settings`, ambas as checagens são
|
|
157
|
+
* no-op (mesma guarda `if (input.settings)` do código original).
|
|
158
|
+
*
|
|
159
|
+
* `input.passwordless === true` pula APENAS a checagem de senha vencida — ver
|
|
160
|
+
* {@link LoginAllowedInput.passwordless}. A expiração por inatividade continua.
|
|
161
|
+
*/
|
|
162
|
+
export async function assertAccountNotExpired(cfg, accountId, input) {
|
|
163
|
+
const { email, ip, clientId, settings, logger } = input;
|
|
164
|
+
if (settings && input.passwordless !== true) {
|
|
165
|
+
const expired = await isPasswordExpired(cfg, accountId, settings);
|
|
166
|
+
if (expired) {
|
|
167
|
+
await cfg.audit?.record(clientId !== undefined
|
|
168
|
+
? { type: 'password.expired_change_forced', accountId, email, ip, clientId }
|
|
169
|
+
: { type: 'password.expired_change_forced', accountId, email, ip });
|
|
170
|
+
return { allowed: false, reason: 'password_expired' };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (settings) {
|
|
174
|
+
const expired = await isAccountExpired(cfg.audit, accountId, settings, logger);
|
|
175
|
+
if (expired) {
|
|
176
|
+
await cfg.audit?.record(clientId !== undefined
|
|
177
|
+
? { type: 'account.expired_login_blocked', accountId, email, ip, clientId }
|
|
178
|
+
: { type: 'account.expired_login_blocked', accountId, email, ip });
|
|
179
|
+
return { allowed: false, reason: 'account_expired' };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { allowed: true };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Gate combinado (disabled → password_expired → account_expired), na ordem
|
|
186
|
+
* canônica. É o que os fluxos passwordless (magic link, OTP, passkey, social)
|
|
187
|
+
* e o token-exchange (impersonation) chamam UMA VEZ, imediatamente antes de
|
|
188
|
+
* `completeLogin` — eles não têm o gate de e-mail-não-verificado intercalado
|
|
189
|
+
* entre o disabled-check e os expiry-checks como `attemptPasswordLogin` tem,
|
|
190
|
+
* então uma chamada única e sequencial é observacionalmente idêntica a chamar
|
|
191
|
+
* os dois gates separados nessa ordem.
|
|
192
|
+
*
|
|
193
|
+
* `attemptPasswordLogin` NÃO usa este combinado — chama `assertAccountEnabled`
|
|
194
|
+
* e `assertAccountNotExpired` separadamente, com o gate de e-mail não
|
|
195
|
+
* verificado (`isEmailUnverifiedBlock`) intercalado entre os dois, para
|
|
196
|
+
* preservar a ordem de checagens original byte-a-byte.
|
|
197
|
+
*/
|
|
198
|
+
export async function assertLoginAllowed(cfg, accountId, input) {
|
|
199
|
+
const enabled = await assertAccountEnabled(cfg, accountId, input);
|
|
200
|
+
if (!enabled.allowed)
|
|
201
|
+
return enabled;
|
|
202
|
+
return assertAccountNotExpired(cfg, accountId, input);
|
|
203
|
+
}
|
|
127
204
|
/**
|
|
128
205
|
* Sequência canônica de login por senha + bloqueio progressivo, compartilhada
|
|
129
206
|
* pelos dois fluxos que pedem senha (interaction OIDC e console de conta):
|
|
@@ -165,17 +242,14 @@ export async function attemptPasswordLogin(cfg, input) {
|
|
|
165
242
|
// Conta desabilitada: rejeita o login (mesmo com senha correta). A capacidade é
|
|
166
243
|
// opcional — só checada quando o store a implementa. Emite `login.failure` (com
|
|
167
244
|
// motivo `disabled` no metadata) e NÃO registra falha no lockout (não é tentativa
|
|
168
|
-
// de adivinhar senha).
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
metadata: { reason: 'disabled' },
|
|
177
|
-
}
|
|
178
|
-
: { type: 'login.failure', email, ip, metadata: { reason: 'disabled' } });
|
|
245
|
+
// de adivinhar senha). Extraído em `assertAccountEnabled` — reutilizado por TODO
|
|
246
|
+
// fluxo de login (magic link, OTP, passkey, social, token-exchange), não só este.
|
|
247
|
+
const enabledCheck = await assertAccountEnabled(cfg, account.id, {
|
|
248
|
+
email,
|
|
249
|
+
ip,
|
|
250
|
+
clientId: input.clientId,
|
|
251
|
+
});
|
|
252
|
+
if (!enabledCheck.allowed) {
|
|
179
253
|
return { ok: false, locked: false, disabled: true };
|
|
180
254
|
}
|
|
181
255
|
// E-mail não verificado (LGPD/compliance): rejeita o login se a política exige
|
|
@@ -194,45 +268,24 @@ export async function attemptPasswordLogin(cfg, input) {
|
|
|
194
268
|
: { type: 'login.failure', email, ip, metadata: { reason: 'unverified' } });
|
|
195
269
|
return { ok: false, locked: false, unverified: true };
|
|
196
270
|
}
|
|
197
|
-
// Senha expirada (password expiration)
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
: { type: 'password.expired_change_forced', accountId: account.id, email, ip });
|
|
271
|
+
// Senha expirada (password expiration) e conta expirada por inatividade
|
|
272
|
+
// (account_expiration): extraído em `assertAccountNotExpired`, na mesma ordem
|
|
273
|
+
// e com os mesmos eventos de auditoria do código original. Capability/setting
|
|
274
|
+
// -probed internamente (sem `input.settings` → no-op, como antes).
|
|
275
|
+
const expiryCheck = await assertAccountNotExpired(cfg, account.id, {
|
|
276
|
+
email,
|
|
277
|
+
ip,
|
|
278
|
+
clientId: input.clientId,
|
|
279
|
+
settings: input.settings,
|
|
280
|
+
logger: input.logger,
|
|
281
|
+
});
|
|
282
|
+
if (!expiryCheck.allowed) {
|
|
283
|
+
// Nenhuma das duas é falha de credenciais — limpa o contador.
|
|
284
|
+
await lockout.clearFailures(email);
|
|
285
|
+
if (expiryCheck.reason === 'password_expired') {
|
|
214
286
|
return { ok: false, locked: false, passwordExpired: true, account: account };
|
|
215
287
|
}
|
|
216
|
-
|
|
217
|
-
// Conta expirada por inatividade (account_expiration setting): bloqueia o login
|
|
218
|
-
// se a conta não teve login.success há mais de `inactiveDays` dias (via audit).
|
|
219
|
-
// Capability-probed: sem audit.list ou sem setting → no-op.
|
|
220
|
-
// Reativação: fluxo de reset de senha (link exibido pelo controller na mensagem).
|
|
221
|
-
if (input.settings) {
|
|
222
|
-
const expired = await isAccountExpired(cfg.audit, account.id, input.settings, input.logger);
|
|
223
|
-
if (expired) {
|
|
224
|
-
await lockout.clearFailures(email);
|
|
225
|
-
await cfg.audit?.record(input.clientId !== undefined
|
|
226
|
-
? {
|
|
227
|
-
type: 'account.expired_login_blocked',
|
|
228
|
-
accountId: account.id,
|
|
229
|
-
email,
|
|
230
|
-
ip,
|
|
231
|
-
clientId: input.clientId,
|
|
232
|
-
}
|
|
233
|
-
: { type: 'account.expired_login_blocked', accountId: account.id, email, ip });
|
|
234
|
-
return { ok: false, locked: false, accountExpired: true };
|
|
235
|
-
}
|
|
288
|
+
return { ok: false, locked: false, accountExpired: true };
|
|
236
289
|
}
|
|
237
290
|
// Senha correta: limpa o contador de falhas (o lockout protege a etapa de senha).
|
|
238
291
|
await lockout.clearFailures(email);
|
|
@@ -98,7 +98,7 @@ async function notifyNewDevice(ctx, cfg, data) {
|
|
|
98
98
|
});
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
await sendNewDeviceLoginEmail(ctx, {
|
|
101
|
+
await sendNewDeviceLoginEmail(ctx, cfg, {
|
|
102
102
|
email: data.email,
|
|
103
103
|
ip: data.ip,
|
|
104
104
|
userAgent: data.userAgent,
|
|
@@ -132,7 +132,7 @@ async function maybeNotifyNewLogin(ctx, cfg, data) {
|
|
|
132
132
|
if (sameIpCount > 1)
|
|
133
133
|
return;
|
|
134
134
|
const when = new Date().toISOString();
|
|
135
|
-
await sendNewLoginEmail(ctx, { email, ip, when });
|
|
135
|
+
await sendNewLoginEmail(ctx, cfg, { email, ip, when });
|
|
136
136
|
await cfg.audit?.record({
|
|
137
137
|
type: 'login.new_ip_notified',
|
|
138
138
|
accountId,
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
import { symbols } from '@adonisjs/auth';
|
|
1
|
+
import type { symbols } from '@adonisjs/auth';
|
|
2
2
|
import type { AuthClientResponse, GuardConfigProvider, GuardContract } from '@adonisjs/auth/types';
|
|
3
3
|
import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
|
|
4
4
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
5
5
|
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
6
6
|
import type { EmitterLike } from '@adonisjs/core/types/events';
|
|
7
7
|
type RealUser<UserProvider> = UserProvider extends SessionUserProviderContract<infer U> ? U : never;
|
|
8
|
+
/**
|
|
9
|
+
* Construtor do `E_UNAUTHORIZED_ACCESS` do `@adonisjs/auth` — a MESMA classe que
|
|
10
|
+
* os guards nativos lançam (`static status = 401`, `redirectTo`, renderers
|
|
11
|
+
* html/json/jsonapi). Tipado à mão porque o pacote não pode ser importado
|
|
12
|
+
* estaticamente daqui (peer opcional).
|
|
13
|
+
*/
|
|
14
|
+
export type UnauthorizedAccessConstructor = new (message: string, options: {
|
|
15
|
+
guardDriverName: string;
|
|
16
|
+
redirectTo?: string;
|
|
17
|
+
}) => Error;
|
|
18
|
+
/**
|
|
19
|
+
* Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
|
|
20
|
+
* estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
|
|
21
|
+
* mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
|
|
22
|
+
* primeira `authenticate()` de um guard construído à mão.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadUnauthorizedAccess(): Promise<UnauthorizedAccessConstructor>;
|
|
8
25
|
export type OidcRpGuardOptions<UserProvider extends SessionUserProviderContract<unknown>> = {
|
|
9
26
|
provider: UserProvider | ConfigProvider<UserProvider>;
|
|
10
27
|
sessionKey?: string;
|
|
@@ -63,7 +80,7 @@ export declare class OidcRpGuard<UserProvider extends SessionUserProviderContrac
|
|
|
63
80
|
isAuthenticated: boolean;
|
|
64
81
|
isLoggedOut: boolean;
|
|
65
82
|
user?: RealUser<UserProvider>;
|
|
66
|
-
constructor(name: string, ctx: HttpContext, sessionKey: string, emitter: EmitterLike<OidcRpGuardEvents<RealUser<UserProvider>>>, userProvider: UserProvider);
|
|
83
|
+
constructor(name: string, ctx: HttpContext, sessionKey: string, emitter: EmitterLike<OidcRpGuardEvents<RealUser<UserProvider>>>, userProvider: UserProvider, unauthorized?: UnauthorizedAccessConstructor);
|
|
67
84
|
getUserOrFail(): RealUser<UserProvider>;
|
|
68
85
|
/**
|
|
69
86
|
* Grava a identidade na sessão. Chamado pelo callback OIDC após validar o
|
|
@@ -77,6 +94,12 @@ export declare class OidcRpGuard<UserProvider extends SessionUserProviderContrac
|
|
|
77
94
|
*/
|
|
78
95
|
logout(): Promise<void>;
|
|
79
96
|
authenticate(): Promise<RealUser<UserProvider>>;
|
|
97
|
+
/**
|
|
98
|
+
* `authenticate()` sem lançar — mas SÓ para falha de autenticação. Igual ao
|
|
99
|
+
* `SessionGuard` nativo: engole apenas `E_UNAUTHORIZED_ACCESS` e relança o
|
|
100
|
+
* resto. Um `catch` cego aqui transformava uma queda do banco dentro de
|
|
101
|
+
* `provider.findById` em "não logado" para todo mundo, sem nada nos logs.
|
|
102
|
+
*/
|
|
80
103
|
check(): Promise<boolean>;
|
|
81
104
|
authenticateAsClient(user: RealUser<UserProvider>): Promise<AuthClientResponse>;
|
|
82
105
|
}
|