@adonis-agora/authkit-server 0.45.0 → 0.47.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.
Files changed (64) hide show
  1. package/build/commands/ui_preset.js +15 -1
  2. package/build/host/views/account/confirm.edge +118 -52
  3. package/build/host/views/account/mfa.edge +1 -1
  4. package/build/host/views/login.edge +2 -4
  5. package/build/host/views/mfa-challenge.edge +1 -1
  6. package/build/host/views/otp-unlock.edge +2 -2
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +5 -1
  9. package/build/index.js +15 -1
  10. package/build/providers/authkit_server_provider.js +7 -0
  11. package/build/services/booted_app.d.ts +8 -0
  12. package/build/services/booted_app.js +27 -0
  13. package/build/services/main.d.ts +7 -0
  14. package/build/services/main.js +9 -1
  15. package/build/src/define_config.d.ts +51 -0
  16. package/build/src/define_config.js +8 -0
  17. package/build/src/host/account_login_url.d.ts +41 -0
  18. package/build/src/host/account_login_url.js +50 -0
  19. package/build/src/host/admin_sessions_service.d.ts +3 -1
  20. package/build/src/host/admin_sessions_service.js +35 -3
  21. package/build/src/host/assets/webauthn.js +2 -0
  22. package/build/src/host/console_session.d.ts +4 -0
  23. package/build/src/host/console_session.js +9 -2
  24. package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
  25. package/build/src/host/controllers/account_confirm_controller.js +42 -130
  26. package/build/src/host/controllers/account_orgs_controller.js +7 -4
  27. package/build/src/host/controllers/account_security_controller.js +3 -2
  28. package/build/src/host/controllers/account_session_controller.js +23 -5
  29. package/build/src/host/controllers/account_tokens_controller.js +11 -0
  30. package/build/src/host/controllers/pat_introspection_controller.js +10 -0
  31. package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
  32. package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
  33. package/build/src/host/i18n.d.ts +14 -0
  34. package/build/src/host/i18n.js +16 -0
  35. package/build/src/host/impersonation_session.js +15 -0
  36. package/build/src/host/middleware/account_auth.js +3 -1
  37. package/build/src/host/rate_limit.d.ts +10 -0
  38. package/build/src/host/rate_limit.js +6 -0
  39. package/build/src/host/register_auth_host.d.ts +85 -2
  40. package/build/src/host/register_auth_host.js +188 -61
  41. package/build/src/host/renderers/edge_renderer.js +6 -1
  42. package/build/src/host/renderers/inertia_renderer.d.ts +55 -0
  43. package/build/src/host/renderers/inertia_renderer.js +55 -0
  44. package/build/src/host/sudo/index.d.ts +47 -0
  45. package/build/src/host/sudo/index.js +41 -0
  46. package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
  47. package/build/src/host/sudo/methods/magic_link.js +174 -0
  48. package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
  49. package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
  50. package/build/src/host/sudo/methods/passkey.d.ts +28 -0
  51. package/build/src/host/sudo/methods/passkey.js +139 -0
  52. package/build/src/host/sudo/methods/password.d.ts +19 -0
  53. package/build/src/host/sudo/methods/password.js +93 -0
  54. package/build/src/host/sudo/runtime.d.ts +141 -0
  55. package/build/src/host/sudo/runtime.js +327 -0
  56. package/build/src/host/sudo/types.d.ts +93 -0
  57. package/build/src/host/sudo/types.js +1 -0
  58. package/build/src/host/sudo_mode.d.ts +116 -6
  59. package/build/src/host/sudo_mode.js +133 -7
  60. package/package.json +6 -2
  61. package/build/stubs/ui/edge/views/consent.edge +0 -13
  62. package/build/stubs/ui/edge/views/login.edge +0 -19
  63. package/stubs/ui/edge/views/consent.edge +0 -13
  64. package/stubs/ui/edge/views/login.edge +0 -19
@@ -0,0 +1,22 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * GET /authkit/assets/webauthn.js
4
+ *
5
+ * Serve o `@simplewebauthn/browser` a partir do próprio host, substituindo o
6
+ * import de `cdn.jsdelivr.net` que as views de login/MFA/confirm faziam.
7
+ *
8
+ * SEM AUTENTICAÇÃO, e é intencional: é asset estático necessário na tela de
9
+ * login, ou seja, antes de existir qualquer sessão. Também não pode viver sob
10
+ * o prefixo do console admin (que é opt-in) — `login.edge` e
11
+ * `mfa-challenge.edge` precisam do script mesmo num host sem console.
12
+ */
13
+ export default class WebauthnAssetController {
14
+ handle(ctx: HttpContext): Promise<void>;
15
+ }
16
+ /**
17
+ * Limpa o cache do bundle. Existe para os testes conseguirem exercitar tanto o
18
+ * caminho feliz quanto o 404 no mesmo processo.
19
+ *
20
+ * @internal
21
+ */
22
+ export declare function resetWebauthnAssetCache(): void;
@@ -0,0 +1,66 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ /**
3
+ * URL do bundle ESM do `@simplewebauthn/browser` empacotado por
4
+ * `scripts/build_webauthn.mjs`.
5
+ *
6
+ * ⚠️ RESOLVIDO VIA `import.meta.url`, NUNCA relativo ao cwd. Os apps que
7
+ * consomem este pacote rodam `pnpm deploy --legacy`, que remonta a árvore de
8
+ * `node_modules` num diretório novo — um caminho relativo ao cwd do host
9
+ * apontaria para o lugar errado e a rota daria 404 em produção. Ancorado no
10
+ * módulo, o caminho segue o pacote para onde quer que ele seja copiado.
11
+ *
12
+ * Funciona nos dois layouts porque o bundle é commitado em `src/host/assets/`
13
+ * e copiado para `build/src/host/assets/` pelo script `build`:
14
+ * dev → src/host/controllers/… → src/host/assets/webauthn.js
15
+ * build → build/src/host/controllers/… → build/src/host/assets/webauthn.js
16
+ */
17
+ const BUNDLE_URL = new URL('../assets/webauthn.js', import.meta.url);
18
+ /**
19
+ * Cache do conteúdo do bundle. É imutável por versão do pacote — ler do disco
20
+ * a cada request de tela de login não compra nada.
21
+ *
22
+ * `null` = ainda não lido. `false` = lido e ausente (404 memoizado); sem isso
23
+ * um bundle faltando viraria um `readFile` que falha por request.
24
+ */
25
+ let cached = null;
26
+ /**
27
+ * GET /authkit/assets/webauthn.js
28
+ *
29
+ * Serve o `@simplewebauthn/browser` a partir do próprio host, substituindo o
30
+ * import de `cdn.jsdelivr.net` que as views de login/MFA/confirm faziam.
31
+ *
32
+ * SEM AUTENTICAÇÃO, e é intencional: é asset estático necessário na tela de
33
+ * login, ou seja, antes de existir qualquer sessão. Também não pode viver sob
34
+ * o prefixo do console admin (que é opt-in) — `login.edge` e
35
+ * `mfa-challenge.edge` precisam do script mesmo num host sem console.
36
+ */
37
+ export default class WebauthnAssetController {
38
+ async handle(ctx) {
39
+ if (cached === null) {
40
+ try {
41
+ cached = await readFile(BUNDLE_URL);
42
+ }
43
+ catch {
44
+ cached = false;
45
+ }
46
+ }
47
+ if (cached === false) {
48
+ // 404 limpo: o bundle não foi gerado (`node scripts/build_webauthn.mjs`).
49
+ // A tela degrada para os demais fatores em vez de estourar 500.
50
+ return ctx.response.notFound();
51
+ }
52
+ return ctx.response
53
+ .type('text/javascript')
54
+ .header('Cache-Control', 'public, max-age=31536000, immutable')
55
+ .send(cached);
56
+ }
57
+ }
58
+ /**
59
+ * Limpa o cache do bundle. Existe para os testes conseguirem exercitar tanto o
60
+ * caminho feliz quanto o 404 no mesmo processo.
61
+ *
62
+ * @internal
63
+ */
64
+ export function resetWebauthnAssetCache() {
65
+ cached = null;
66
+ }
@@ -560,6 +560,13 @@ export declare const DEFAULT_MESSAGES: {
560
560
  "account.confirm.error": string;
561
561
  "account.confirm.passkey_error": string;
562
562
  "account.confirm.passwordless_notice": string;
563
+ "account.confirm.method.password": string;
564
+ "account.confirm.method.passkey": string;
565
+ "account.confirm.method.magic_link": string;
566
+ "account.confirm.method.oidc_step_up": string;
567
+ "account.confirm.magic_link_sent": string;
568
+ "account.confirm.no_methods": string;
569
+ "account.confirm.preferred_badge": string;
563
570
  "admin.settings.sudo_mode_section": string;
564
571
  "admin.settings.sudo_mode_intro": string;
565
572
  "admin.settings.sudo_mode_from_config": string;
@@ -1242,6 +1249,13 @@ export declare const PT_BR_MESSAGES: {
1242
1249
  "account.confirm.error": string;
1243
1250
  "account.confirm.passkey_error": string;
1244
1251
  "account.confirm.passwordless_notice": string;
1252
+ "account.confirm.method.password": string;
1253
+ "account.confirm.method.passkey": string;
1254
+ "account.confirm.method.magic_link": string;
1255
+ "account.confirm.method.oidc_step_up": string;
1256
+ "account.confirm.magic_link_sent": string;
1257
+ "account.confirm.no_methods": string;
1258
+ "account.confirm.preferred_badge": string;
1245
1259
  "admin.settings.sudo_mode_section": string;
1246
1260
  "admin.settings.sudo_mode_intro": string;
1247
1261
  "admin.settings.sudo_mode_from_config": string;
@@ -605,6 +605,14 @@ export const DEFAULT_MESSAGES = {
605
605
  "account.confirm.error": "Incorrect password.",
606
606
  "account.confirm.passkey_error": "Could not authenticate with the passkey. Please try again.",
607
607
  "account.confirm.passwordless_notice": "This account does not have a password. Please add a passkey to use sudo-protected features.",
608
+ // Rótulos dos métodos do SPI de sudo (account/confirm.edge, um bloco por método disponível).
609
+ "account.confirm.method.password": "Confirm with your password",
610
+ "account.confirm.method.passkey": "Confirm with a passkey",
611
+ "account.confirm.method.magic_link": "Email me a confirmation link",
612
+ "account.confirm.method.oidc_step_up": "Sign in again to confirm",
613
+ "account.confirm.magic_link_sent": "We sent a confirmation link to your email. It expires in 5 minutes.",
614
+ "account.confirm.no_methods": "No confirmation method is available for this account. Contact support.",
615
+ "account.confirm.preferred_badge": "Used last time",
608
616
  // Admin settings — sudo_mode card.
609
617
  "admin.settings.sudo_mode_section": "Sudo mode (identity confirmation)",
610
618
  "admin.settings.sudo_mode_intro": "When enabled, sensitive actions (password change, email change, account deletion, MFA/passkey management, PAT creation/revocation) require the user to confirm their password. The confirmation is valid for a configurable grace period.",
@@ -1359,6 +1367,14 @@ export const PT_BR_MESSAGES = {
1359
1367
  "account.confirm.error": "Senha incorreta.",
1360
1368
  "account.confirm.passkey_error": "Não foi possível autenticar com a passkey. Tente novamente.",
1361
1369
  "account.confirm.passwordless_notice": "Esta conta não possui senha. Adicione uma passkey para usar funcionalidades protegidas.",
1370
+ // Rótulos dos métodos do SPI de sudo (account/confirm.edge, um bloco por método disponível).
1371
+ "account.confirm.method.password": "Confirmar com a senha",
1372
+ "account.confirm.method.passkey": "Confirmar com passkey",
1373
+ "account.confirm.method.magic_link": "Receber link de confirmação por e-mail",
1374
+ "account.confirm.method.oidc_step_up": "Entrar de novo para confirmar",
1375
+ "account.confirm.magic_link_sent": "Enviamos um link de confirmação para o seu e-mail. Ele expira em 5 minutos.",
1376
+ "account.confirm.no_methods": "Nenhum método de confirmação está disponível para esta conta. Fale com o suporte.",
1377
+ "account.confirm.preferred_badge": "Usado da última vez",
1362
1378
  // Admin settings — sudo_mode card (pt-BR).
1363
1379
  "admin.settings.sudo_mode_section": "Modo sudo (confirmação de identidade)",
1364
1380
  "admin.settings.sudo_mode_intro": "Quando habilitado, ações sensíveis (troca de senha, troca de e-mail, exclusão de conta, gerência de MFA/passkey, criação/revogação de PAT) exigem que o usuário confirme sua senha. A confirmação é válida por um período de graça configurável.",
@@ -98,6 +98,14 @@ export async function startImpersonation(ctx, params) {
98
98
  // Anti-fixation: rotaciona o id da sessão (mantém os dados) antes de gravar a
99
99
  // nova identidade. Mesmo padrão do consumidor real no RP.
100
100
  await ctx.session.regenerate();
101
+ // ESCALAÇÃO DE PRIVILÉGIO (fechada por vinculação): trocar a conta aqui NÃO
102
+ // pode carregar junto o sudo que o admin confirmou sobre a PRÓPRIA conta —
103
+ // senão ele entraria personificando já com a graça aberta sobre a conta
104
+ // alheia (exportar/excluir dados, MFA, PATs). Não limpamos a marca aqui: ela
105
+ // é vinculada à conta que a confirmou (`SUDO_ACCOUNT_SESSION_KEY`), então
106
+ // `isSudoActive` a recusa sozinho assim que `ACCOUNT_SESSION_KEY` muda. A
107
+ // garantia é estrutural — vale para qualquer troca de conta futura, sem
108
+ // depender de um `forget` lembrado em cada nova transição.
101
109
  ctx.session.put(IMPERSONATOR_SESSION_KEY, impersonatorId);
102
110
  ctx.session.put(ACCOUNT_SESSION_KEY, params.targetId);
103
111
  }
@@ -122,6 +130,13 @@ export async function stopImpersonation(ctx) {
122
130
  if (!impersonatorId)
123
131
  return;
124
132
  await ctx.session.regenerate();
133
+ // Simétrico ao `startImpersonation`: o sudo obtido ENQUANTO personificava
134
+ // ficaria valendo sobre a conta do admin ao voltar. A vinculação corta isso —
135
+ // aquela marca aponta para a conta personificada e morre com a volta.
136
+ //
137
+ // Nota: se o admin tinha sudo sobre a própria conta ANTES de personificar e a
138
+ // graça ainda não venceu, ele volta valendo. Correto e intencional: é a
139
+ // confirmação dele, sobre a conta dele, dentro da janela dele.
125
140
  ctx.session.put(ACCOUNT_SESSION_KEY, impersonatorId);
126
141
  ctx.session.forget(IMPERSONATOR_SESSION_KEY);
127
142
  ctx.session.forget(ADMIN_ACCESS_TOKEN_SESSION_KEY);
@@ -1,10 +1,12 @@
1
1
  import '../augmentations.js';
2
+ import { getAccountLoginUrl } from '../account_login_url.js';
2
3
  export const ACCOUNT_SESSION_KEY = 'account_user_id';
3
4
  export default class AccountAuthMiddleware {
4
5
  async handle(ctx, next) {
5
6
  const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
6
7
  if (!userId) {
7
- return ctx.response.redirect('/account/login');
8
+ // Destino configurável (`accountLoginUrl`): default `/account/login`.
9
+ return ctx.response.redirect(getAccountLoginUrl());
8
10
  }
9
11
  return next();
10
12
  }
@@ -20,6 +20,16 @@ export interface AuthThrottles {
20
20
  * de tentativas vindas do MESMO IP independentemente de qual key foi usada (M8).
21
21
  */
22
22
  adminIp: ThrottleMiddleware;
23
+ /**
24
+ * Throttle das rotas dos métodos de sudo (`/account/confirm/*`), keyed por IP
25
+ * como o `login` — e com os MESMOS limites — mas em bucket PRÓPRIO.
26
+ *
27
+ * A separação é o ponto: `login` mede um anônimo tentando adivinhar
28
+ * credenciais, `sudo` mede um usuário já autenticado reprovando a própria
29
+ * identidade. Ver `ResolvedRateLimitConfig.sudo` para o porquê de os dois
30
+ * orçamentos não poderem se consumir.
31
+ */
32
+ sudo: ThrottleMiddleware;
23
33
  }
24
34
  /**
25
35
  * Service do `@adonisjs/limiter` resolvido de forma preguiçosa. Tipado como `any`
@@ -90,5 +90,11 @@ export function createAuthThrottles(config) {
90
90
  adminIp: buildThrottle('authkit_admin_ip', config.adminIp, config.store, (ctx) => {
91
91
  return `admin-ip:${ctx.request.ip?.() ?? 'unknown'}`;
92
92
  }),
93
+ // Rotas dos métodos de sudo: keyed por IP (default do HttpLimiter), igual ao
94
+ // login. O que separa os dois é o NOME do bucket — o limiter namespaceia a
95
+ // contagem por nome, então `authkit_login` e `authkit_sudo` nunca somam,
96
+ // mesmo vindo do mesmo IP. Sem `usingKey` próprio de propósito: inventar uma
97
+ // key aqui seria mudar o EIXO da contagem, e o eixo certo continua sendo o IP.
98
+ sudo: buildThrottle('authkit_sudo', config.sudo, config.store),
93
99
  };
94
100
  }
@@ -1,14 +1,17 @@
1
1
  import type { Router } from '@adonisjs/core/http';
2
2
  import type { AuthSocialConfig, RateLimitConfigInput } from '../define_config.js';
3
+ import type { SudoMethod } from './sudo/types.js';
3
4
  /** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
4
5
  export declare const ACCOUNT_LAST_SEEN_KEY = "authkit_last_seen";
5
6
  /**
6
7
  * Guard do console admin (B6). Como o `accountGuard`, é uma closure inline (forma
7
8
  * confiável do `.use()` num grupo). Exige:
8
9
  * 0. `config.admin.enabled` ligado (senão → 404; ver nota de flag-drift abaixo);
9
- * 1. sessão de conta ativa (senão → /account/login);
10
+ * 1. sessão de conta ativa (senão → `accountLoginUrl`, default /account/login);
10
11
  * 2. a conta logada com pelo menos UMA das `config.admin.roles` nas roles globais
11
- * (senão → /account/tokens, evitando vazar a existência do /admin).
12
+ * (senão → `accountHome(cfg)`, default /account/security NÃO revela a
13
+ * existência do /admin, e cai numa tela que o host controla via `accountHome`;
14
+ * se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
12
15
  * As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
13
16
  */
14
17
  export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promise<any>;
@@ -89,6 +92,86 @@ export interface AuthHostOptions {
89
92
  adminApi?: boolean | {
90
93
  prefix?: string;
91
94
  };
95
+ /**
96
+ * Métodos de sudo cujas rotas devem ser montadas. Necessário aqui (e não só
97
+ * no config) porque a decisão de MONTAR rotas acontece em tempo de registro,
98
+ * antes de o config lazy resolver — mesma razão de `social`/`admin`/`rateLimit`.
99
+ * Espelhe o `sudo.methods` de config/authkit.ts.
100
+ *
101
+ * SUBSTITUI os defaults, não acrescenta: a lista é do host. Quem quer manter
102
+ * senha/passkey ao lado do método novo os inclui explicitamente
103
+ * (`[sudoMethods.password(), sudoMethods.passkey(), meuMetodo()]`).
104
+ *
105
+ * Sem esta opção, `config.sudo.methods` conseguiria OFERECER um método na
106
+ * tela mas nunca montar seu endpoint — a opção aparece e dá 404. Falha
107
+ * fechada, mas é a promessa do SPI pela metade; `magicLink()` em particular
108
+ * não teria como ser alcançado em runtime.
109
+ *
110
+ * Ausente → `[password(), passkey()]`.
111
+ */
112
+ sudoMethods?: SudoMethod[];
113
+ /**
114
+ * Montagem por tela do console de conta (`/account/*`). Espelha o padrão
115
+ * `admin`/`adminApi`: a decisão de MONTAR cada grupo de rotas é tomada em
116
+ * tempo de registro, antes de o config (lazy) resolver.
117
+ *
118
+ * - Ausente → tudo montado (back-compat total).
119
+ * - `false` → NENHUMA tela do console de conta é montada (as rotas sudo
120
+ * `/account/confirm` e a JSON API `/account/api/*` continuam — são
121
+ * infraestrutura, não telas navegáveis).
122
+ * - objeto → montagem seletiva; cada flag ausente default `true`.
123
+ * - `login` → `/account/login`, `/account/logout` (a porta por senha do console).
124
+ * - `tokens` → `/account/tokens*` (Personal Access Tokens).
125
+ * - `orgs` → `/account/orgs*` (multi-tenancy, incl. o accept de convite público).
126
+ * - `security` → `/account/security*` + `/account/email/confirm` (perfil, senha, troca de e-mail, export/LGPD, deleção).
127
+ * - `mfa` → `/account/mfa*` (TOTP + passkeys).
128
+ * - `apps` → `/account/apps*` (grants de consentimento OIDC).
129
+ *
130
+ * NOTA (flag-drift): como `admin`/`adminApi`, estas flags controlam apenas se
131
+ * as ROTAS existem; o comportamento em runtime continua vindo do config
132
+ * resolvido. Mantenha em sincronia — os guards são a rede de segurança.
133
+ *
134
+ * ⚠️ Ao desmontar `login`, os redirects internos de "faça login"
135
+ * (`accountGuard`, `adminGuard`, `AccountAuthMiddleware`, `consoleLoginUrl()`,
136
+ * a view `otp-unlock`) apontariam para uma rota inexistente — passe
137
+ * `accountLoginUrl` com a rota de login do host (ex.: `'/login'`).
138
+ *
139
+ * @example
140
+ * // Console passwordless: só segurança + MFA, login delegado ao OIDC do host.
141
+ * registerAuthHost(router, {
142
+ * account: { login: false, tokens: false, orgs: false },
143
+ * accountLoginUrl: '/login',
144
+ * })
145
+ */
146
+ account?: false | AccountScreensOptions;
147
+ /**
148
+ * Destino do redirect de "não-autenticado → faça login" do console de conta.
149
+ * Default `/account/login`. Aponte para a rota de login do host quando a tela
150
+ * `account/login` da lib estiver desmontada (`account: { login: false }`).
151
+ *
152
+ * Usado por TODOS os pontos de redirect/link de login: `accountGuard`,
153
+ * `adminGuard`, `AccountAuthMiddleware`, `consoleLoginUrl()`, os fallbacks dos
154
+ * controllers de conta e a view `otp-unlock`. Ver `account_login_url.ts`.
155
+ */
156
+ accountLoginUrl?: string;
157
+ }
158
+ /**
159
+ * Flags de montagem por tela do console de conta. Cada campo ausente é `true`
160
+ * (montado). Ver {@link AuthHostOptions.account}.
161
+ */
162
+ export interface AccountScreensOptions {
163
+ /** Tela de login por senha do console (`/account/login`, `/account/logout`). */
164
+ login?: boolean;
165
+ /** Personal Access Tokens (`/account/tokens*`). */
166
+ tokens?: boolean;
167
+ /** Organizations / multi-tenancy (`/account/orgs*`). */
168
+ orgs?: boolean;
169
+ /** Perfil, senha, troca de e-mail, export/LGPD e deleção (`/account/security*`). */
170
+ security?: boolean;
171
+ /** MFA — TOTP + passkeys (`/account/mfa*`). */
172
+ mfa?: boolean;
173
+ /** Apps com acesso / grants de consentimento OIDC (`/account/apps*`). */
174
+ apps?: boolean;
92
175
  }
93
176
  /**
94
177
  * Monta todas as rotas do host-kit do Authorization Server numa chamada.