@adonis-agora/authkit-server 0.53.0 → 0.55.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 (63) hide show
  1. package/build/index.d.ts +6 -2
  2. package/build/index.js +7 -1
  3. package/build/providers/authkit_server_provider.js +18 -0
  4. package/build/src/accounts/lucid_account_store.d.ts +35 -0
  5. package/build/src/accounts/lucid_account_store.js +9 -0
  6. package/build/src/accounts/lucid_store/core.js +88 -19
  7. package/build/src/accounts/lucid_store/shared.d.ts +14 -0
  8. package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
  9. package/build/src/accounts/lucid_store/token_hash.js +145 -0
  10. package/build/src/define_config.d.ts +93 -8
  11. package/build/src/define_config.js +28 -2
  12. package/build/src/host/account_api/account_api_controller.js +5 -4
  13. package/build/src/host/admin_api/admin_users_service.js +2 -1
  14. package/build/src/host/admin_api/api_orgs_controller.js +2 -1
  15. package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
  16. package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
  17. package/build/src/host/admin_console/console_orgs_controller.js +3 -1
  18. package/build/src/host/admin_validators.d.ts +3 -3
  19. package/build/src/host/auth_host_config.d.ts +30 -0
  20. package/build/src/host/auth_host_config.js +15 -0
  21. package/build/src/host/config_locks.d.ts +29 -0
  22. package/build/src/host/config_locks.js +46 -0
  23. package/build/src/host/console_session.d.ts +38 -2
  24. package/build/src/host/console_session.js +46 -2
  25. package/build/src/host/controllers/account_mfa_controller.js +5 -5
  26. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  27. package/build/src/host/controllers/account_security_controller.js +6 -5
  28. package/build/src/host/controllers/interaction_controller.js +122 -26
  29. package/build/src/host/controllers/registration_controller.js +5 -4
  30. package/build/src/host/controllers/social_controller.js +37 -0
  31. package/build/src/host/default_mailer.d.ts +36 -5
  32. package/build/src/host/default_mailer.js +63 -10
  33. package/build/src/host/i18n.d.ts +10 -0
  34. package/build/src/host/i18n.js +16 -0
  35. package/build/src/host/login_attempt.d.ts +88 -2
  36. package/build/src/host/login_attempt.js +101 -48
  37. package/build/src/host/login_notify.js +2 -2
  38. package/build/src/host/oidc_rp_guard.d.ts +112 -0
  39. package/build/src/host/oidc_rp_guard.js +200 -0
  40. package/build/src/host/origin.d.ts +21 -0
  41. package/build/src/host/origin.js +22 -0
  42. package/build/src/host/register_auth_host.d.ts +104 -3
  43. package/build/src/host/register_auth_host.js +222 -26
  44. package/build/src/host/runtime_settings.d.ts +16 -0
  45. package/build/src/host/runtime_settings.js +24 -0
  46. package/build/src/host/runtime_toggles.d.ts +10 -0
  47. package/build/src/host/runtime_toggles.js +4 -0
  48. package/build/src/host/security_notice_service.d.ts +4 -2
  49. package/build/src/host/security_notice_service.js +4 -2
  50. package/build/src/host/sudo/index.d.ts +8 -0
  51. package/build/src/host/sudo/index.js +8 -0
  52. package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
  53. package/build/src/host/sudo/methods/magic_link.js +37 -13
  54. package/build/src/host/sudo/runtime.d.ts +44 -4
  55. package/build/src/host/sudo/runtime.js +90 -6
  56. package/build/src/host/sudo/satisfiability.d.ts +62 -0
  57. package/build/src/host/sudo/satisfiability.js +89 -0
  58. package/build/src/password/common_passwords.js +27 -7
  59. package/build/src/provider/oidc_service.js +25 -13
  60. package/build/src/provider/token_exchange.d.ts +12 -1
  61. package/build/src/provider/token_exchange.js +12 -0
  62. package/package.json +6 -3
  63. /package/build/{password → src/password}/common_passwords.txt +0 -0
@@ -0,0 +1,200 @@
1
+ import { RuntimeException } from '@adonisjs/core/exceptions';
2
+ import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
3
+ /** Memo do construtor entre instâncias — o `import()` só paga o custo uma vez. */
4
+ let cachedUnauthorizedAccess;
5
+ /**
6
+ * Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
7
+ * estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
8
+ * mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
9
+ * primeira `authenticate()` de um guard construído à mão.
10
+ */
11
+ export async function loadUnauthorizedAccess() {
12
+ if (cachedUnauthorizedAccess)
13
+ return cachedUnauthorizedAccess;
14
+ try {
15
+ const auth = (await import('@adonisjs/auth'));
16
+ cachedUnauthorizedAccess = auth.errors.E_UNAUTHORIZED_ACCESS;
17
+ return cachedUnauthorizedAccess;
18
+ }
19
+ catch (error) {
20
+ throw new RuntimeException('oidcRpGuard() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
21
+ '@adonis-agora/authkit-server, só necessário se você plugar este guard em config/auth.ts). ' +
22
+ 'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
23
+ }
24
+ }
25
+ /**
26
+ * Guard de `@adonisjs/auth` pra Relying Parties OIDC — o app não autentica
27
+ * ninguém (sem senha, sem remember-me); a identidade vem da sessão gravada
28
+ * pelo callback OIDC (`account_user_id`). O guard só LÊ essa chave e resolve
29
+ * o user via provider.
30
+ *
31
+ * ```ts
32
+ * // config/auth.ts
33
+ * import { oidcRpGuard } from '@adonis-agora/authkit-server'
34
+ * import { sessionUserProvider } from '@adonisjs/auth/session'
35
+ *
36
+ * const authConfig = defineConfig({
37
+ * default: 'web',
38
+ * guards: {
39
+ * web: oidcRpGuard({
40
+ * provider: sessionUserProvider({ model: () => import('#models/user') }),
41
+ * }),
42
+ * },
43
+ * })
44
+ * ```
45
+ *
46
+ * O callback OIDC do RP chama `ctx.auth.use('web').login(user)` pra gravar a
47
+ * sessão; a partir daí `ctx.auth.user`, `auth.check()`, `middleware.auth()` —
48
+ * tudo funciona nativamente.
49
+ */
50
+ export class OidcRpGuard {
51
+ #name;
52
+ #ctx;
53
+ #sessionKey;
54
+ #emitter;
55
+ #userProvider;
56
+ #unauthorized;
57
+ driverName = 'oidc_rp';
58
+ authenticationAttempted = false;
59
+ isAuthenticated = false;
60
+ isLoggedOut = false;
61
+ user;
62
+ constructor(name, ctx, sessionKey, emitter, userProvider, unauthorized) {
63
+ this.#name = name;
64
+ this.#ctx = ctx;
65
+ this.#sessionKey = sessionKey;
66
+ this.#emitter = emitter;
67
+ this.#userProvider = userProvider;
68
+ this.#unauthorized = unauthorized ?? cachedUnauthorizedAccess;
69
+ }
70
+ /**
71
+ * O `E_UNAUTHORIZED_ACCESS` do framework — `status` 401 e os renderers
72
+ * html/json, então o handler de exceção do host trata igual ao dos guards
73
+ * nativos. Se o construtor ainda não foi resolvido (guard instanciado à mão,
74
+ * antes de qualquer `authenticate()`), cai num `RuntimeException` em vez de
75
+ * mentir sobre o tipo.
76
+ */
77
+ #unauthorizedError(message) {
78
+ const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccess;
79
+ if (!Unauthorized)
80
+ return new RuntimeException(message);
81
+ return new Unauthorized(message, { guardDriverName: this.driverName });
82
+ }
83
+ getUserOrFail() {
84
+ if (!this.user) {
85
+ throw this.#unauthorizedError('Cannot access user. Authentication has not been attempted or failed.');
86
+ }
87
+ return this.user;
88
+ }
89
+ /**
90
+ * Grava a identidade na sessão. Chamado pelo callback OIDC após validar o
91
+ * grant — o guard NÃO verifica credenciais, só persiste o id do user.
92
+ */
93
+ async login(user) {
94
+ const guardUser = await this.#userProvider.createUserForGuard(user);
95
+ this.#ctx.session.put(this.#sessionKey, String(guardUser.getId()));
96
+ this.user = user;
97
+ this.isAuthenticated = true;
98
+ this.authenticationAttempted = true;
99
+ this.#emitter.emit('oidc_rp:login_succeeded', {
100
+ ctx: this.#ctx,
101
+ guardName: this.#name,
102
+ user,
103
+ });
104
+ }
105
+ /**
106
+ * Limpa a identidade da sessão. O RP-initiated logout (redirect pro
107
+ * `end_session` do issuer) é responsabilidade do controller — o guard só
108
+ * cuida da sessão local.
109
+ */
110
+ async logout() {
111
+ const user = this.user ?? null;
112
+ this.#ctx.session.forget(this.#sessionKey);
113
+ this.user = undefined;
114
+ this.isAuthenticated = false;
115
+ this.isLoggedOut = true;
116
+ this.#emitter.emit('oidc_rp:logged_out', {
117
+ ctx: this.#ctx,
118
+ guardName: this.#name,
119
+ user,
120
+ });
121
+ }
122
+ async authenticate() {
123
+ if (this.authenticationAttempted) {
124
+ return this.getUserOrFail();
125
+ }
126
+ this.authenticationAttempted = true;
127
+ this.#unauthorized ??= await loadUnauthorizedAccess();
128
+ const userId = this.#ctx.session.get(this.#sessionKey);
129
+ if (!userId) {
130
+ this.#emitter.emit('oidc_rp:authentication_failed', {
131
+ ctx: this.#ctx,
132
+ guardName: this.#name,
133
+ });
134
+ throw this.#unauthorizedError('Unauthorized');
135
+ }
136
+ const guardUser = await this.#userProvider.findById(userId);
137
+ if (!guardUser) {
138
+ this.#ctx.session.forget(this.#sessionKey);
139
+ this.#emitter.emit('oidc_rp:authentication_failed', {
140
+ ctx: this.#ctx,
141
+ guardName: this.#name,
142
+ });
143
+ throw this.#unauthorizedError('Unauthorized');
144
+ }
145
+ this.user = guardUser.getOriginal();
146
+ this.isAuthenticated = true;
147
+ this.#emitter.emit('oidc_rp:authentication_succeeded', {
148
+ ctx: this.#ctx,
149
+ guardName: this.#name,
150
+ user: this.user,
151
+ });
152
+ return this.user;
153
+ }
154
+ /**
155
+ * `authenticate()` sem lançar — mas SÓ para falha de autenticação. Igual ao
156
+ * `SessionGuard` nativo: engole apenas `E_UNAUTHORIZED_ACCESS` e relança o
157
+ * resto. Um `catch` cego aqui transformava uma queda do banco dentro de
158
+ * `provider.findById` em "não logado" para todo mundo, sem nada nos logs.
159
+ */
160
+ async check() {
161
+ try {
162
+ await this.authenticate();
163
+ return true;
164
+ }
165
+ catch (error) {
166
+ const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccess;
167
+ if (Unauthorized && error instanceof Unauthorized)
168
+ return false;
169
+ throw error;
170
+ }
171
+ }
172
+ async authenticateAsClient(user) {
173
+ const guardUser = await this.#userProvider.createUserForGuard(user);
174
+ return { session: { [this.#sessionKey]: String(guardUser.getId()) } };
175
+ }
176
+ }
177
+ /**
178
+ * Factory de config pro `oidcRpGuard` — mesmo padrão do `sessionGuard()` de
179
+ * `@adonisjs/auth/session`. Retorna um `GuardConfigProvider` que o
180
+ * `defineConfig` de `config/auth.ts` resolve no boot.
181
+ */
182
+ export function oidcRpGuard(config) {
183
+ return {
184
+ async resolver(name, app) {
185
+ const emitter = await app.container.make('emitter');
186
+ const sessionKey = config.sessionKey ?? ACCOUNT_SESSION_KEY;
187
+ const unauthorized = await loadUnauthorizedAccess();
188
+ let userProvider;
189
+ if (typeof config.provider.resolver === 'function') {
190
+ userProvider = await config.provider.resolver(app);
191
+ }
192
+ else {
193
+ userProvider = config.provider;
194
+ }
195
+ return (ctx) => {
196
+ return new OidcRpGuard(name, ctx, sessionKey, emitter, userProvider, unauthorized);
197
+ };
198
+ },
199
+ };
200
+ }
@@ -0,0 +1,21 @@
1
+ import type { ResolvedServerConfig } from '../define_config.js';
2
+ /**
3
+ * The canonical public origin for links we EMAIL (password reset, magic link,
4
+ * OTP unlock, org invitations, email verification/change, security notices).
5
+ *
6
+ * Never derive these from `request.host()` / `request.protocol()` (which reads
7
+ * `X-Forwarded-Proto` under a trusting proxy config): both are client-supplied.
8
+ * An attacker who can reach the server directly (common when the app sits
9
+ * behind a load balancer that does not pin `Host`) can submit a password-reset
10
+ * or magic-link request for a victim's address with a `Host` of their choosing,
11
+ * and the victim's genuine email ends up pointing at attacker-controlled
12
+ * infrastructure. This is classic password-reset poisoning.
13
+ *
14
+ * Defaults to the origin (`scheme://host[:port]`, no trailing slash, mount
15
+ * path stripped) of the resolved `issuer` — the one canonical origin this
16
+ * library already has. Hosts that legitimately serve the same issuer under
17
+ * multiple public hostnames (and therefore want emailed links to follow the
18
+ * request instead of normalizing to a single hostname) can opt out via the
19
+ * `mail.origin` config escape hatch.
20
+ */
21
+ export declare function authkitOrigin(cfg: ResolvedServerConfig): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The canonical public origin for links we EMAIL (password reset, magic link,
3
+ * OTP unlock, org invitations, email verification/change, security notices).
4
+ *
5
+ * Never derive these from `request.host()` / `request.protocol()` (which reads
6
+ * `X-Forwarded-Proto` under a trusting proxy config): both are client-supplied.
7
+ * An attacker who can reach the server directly (common when the app sits
8
+ * behind a load balancer that does not pin `Host`) can submit a password-reset
9
+ * or magic-link request for a victim's address with a `Host` of their choosing,
10
+ * and the victim's genuine email ends up pointing at attacker-controlled
11
+ * infrastructure. This is classic password-reset poisoning.
12
+ *
13
+ * Defaults to the origin (`scheme://host[:port]`, no trailing slash, mount
14
+ * path stripped) of the resolved `issuer` — the one canonical origin this
15
+ * library already has. Hosts that legitimately serve the same issuer under
16
+ * multiple public hostnames (and therefore want emailed links to follow the
17
+ * request instead of normalizing to a single hostname) can opt out via the
18
+ * `mail.origin` config escape hatch.
19
+ */
20
+ export function authkitOrigin(cfg) {
21
+ return cfg.mail?.origin ?? new URL(cfg.issuer).origin;
22
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Router } from '@adonisjs/core/http';
2
2
  import type { AuthSocialConfig, RateLimitConfigInput } from '../define_config.js';
3
- import { type AccountPathsOptions } from './account_paths.js';
3
+ import { type AccountPathKey, type AccountPathsOptions } from './account_paths.js';
4
+ import type { PolicyRouteOption } from './config_locks.js';
4
5
  import type { SudoMethod } from './sudo/types.js';
5
6
  /** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
6
7
  export declare const ACCOUNT_LAST_SEEN_KEY = "authkit_last_seen";
@@ -108,7 +109,14 @@ export interface AuthHostOptions {
108
109
  * fechada, mas é a promessa do SPI pela metade; `magicLink()` em particular
109
110
  * não teria como ser alcançado em runtime.
110
111
  *
111
- * Ausente → `[password(), passkey()]`.
112
+ * Ausente → `[password(), passkey(), magicLink()]`, e o config resolvido
113
+ * decide quais deles a tela OFERECE: host com senha recebe
114
+ * `[password, passkey]` (o histórico, byte a byte) e host que declarou
115
+ * `authMethods: { password: false }` recebe `[passkey, magicLink]` — sem isso
116
+ * ele não teria um único método satisfazível. Ver `derivedSudoMethods` em
117
+ * `sudo/runtime.ts`.
118
+ *
119
+ * Passar a opção desliga essa derivação: a lista é do host, ao pé da letra.
112
120
  */
113
121
  sudoMethods?: SudoMethod[];
114
122
  /**
@@ -203,8 +211,101 @@ export interface AccountScreensOptions {
203
211
  /** Apps com acesso / grants de consentimento OIDC (`/account/apps*`). */
204
212
  apps?: boolean;
205
213
  }
214
+ /**
215
+ * Mapa RESOLVIDO das rotas montadas, devolvido por `registerAuthHost`.
216
+ *
217
+ * Existe porque os overrides de path (`accountRoutes`) chegavam ao servidor
218
+ * (via `accountPath()`) mas NÃO ao frontend: o layout React e os formulários do
219
+ * host tinham de repetir os mesmos paths à mão, e um override era meio-recurso —
220
+ * certo no servidor, errado na UI. Entregue este mapa ao frontend (uma shared
221
+ * prop do Inertia, um `<script type="application/json">`, um endpoint) em vez de
222
+ * hardcodar `href`s.
223
+ *
224
+ * @example
225
+ * const authkitRoutes = registerAuthHost(router)
226
+ * router.get('/', ({ inertia }) => inertia.render('home', { authkitRoutes }))
227
+ */
228
+ export interface AuthHostRouteMap {
229
+ /** Onde o provider OIDC foi montado (o wildcard é `${mountPath}/*`). */
230
+ mountPath: string;
231
+ /** Console de conta: prefixo, base da JSON API e o path de CADA tela. */
232
+ account: {
233
+ /** Prefixo base resolvido (default `/account`). */
234
+ prefix: string;
235
+ /** Base da JSON API do console de conta (default `/account/api`). */
236
+ api: string;
237
+ /** Path completo de cada tela navegável (`security` → `/account/security`). */
238
+ paths: Record<AccountPathKey, string>;
239
+ /** Destino do redirect de "faça login" em vigor. */
240
+ loginUrl: string;
241
+ /** Quais telas foram efetivamente montadas. */
242
+ screens: Record<keyof AccountScreensOptions, boolean>;
243
+ };
244
+ /** Console admin: prefixo resolvido, ou `null` quando não foi montado. */
245
+ admin: {
246
+ prefix: string;
247
+ } | null;
248
+ /** Admin REST API: prefixo resolvido, ou `null` quando não foi montada. */
249
+ adminApi: {
250
+ prefix: string;
251
+ } | null;
252
+ /** Nomes das rotas nomeadas (as demais herdam o auto-naming do AdonisJS). */
253
+ names: Record<string, string>;
254
+ /** Ids dos métodos de sudo cujas rotas foram montadas, na ordem de montagem. */
255
+ sudoMethods: string[];
256
+ /**
257
+ * Opções de POLÍTICA passadas como argumento que foram IGNORADAS porque o
258
+ * `defineConfig` as declarou (config vence). Vazio no caso normal. Cada uma
259
+ * também sai como `console.warn` no boot — ver a regra de precedência abaixo.
260
+ */
261
+ overriddenByConfig: PolicyRouteOption[];
262
+ }
206
263
  /**
207
264
  * Monta todas as rotas do host-kit do Authorization Server numa chamada.
208
265
  * Substitui registerOidcRoutes + o hand-wiring do start/routes.ts do host.
266
+ *
267
+ * ── A REGRA DE PRECEDÊNCIA (config × argumento) ─────────────────────────────
268
+ *
269
+ * 1. **Argumento omitido HERDA do config.** `registerAuthHost(router)` é
270
+ * totalmente config-driven; `registerAuthHost(router, { mountPath: '/sso' })`
271
+ * troca o mountPath e herda TODO o resto. Omitir uma chave significa "usa o
272
+ * config", nunca "usa nada" — é o que dispensa repetir `sudo.methods` no
273
+ * `start/routes.ts`.
274
+ *
275
+ * 2. **Chaves ESTRUTURAIS: o argumento vence.** `mountPath`, os prefixos
276
+ * (`admin.prefix`, `adminApi.prefix`, `accountRoutes.prefix`), os segmentos
277
+ * de tela, `account` (quais telas montar) e `accountLoginUrl`. São decisões
278
+ * do ponto de chamada por natureza, e a forma de função é estritamente mais
279
+ * expressiva (dá para montar duas vezes sob dois prefixos — nenhum config
280
+ * expressa isso).
281
+ *
282
+ * 3. **Chaves de POLÍTICA: o config vence, e trava.** `social`, `rateLimit`,
283
+ * `sudoMethods` e o liga/desliga de `admin`/`adminApi` decidem o que é
284
+ * PERMITIDO. Quando o `defineConfig` as declara, o argumento NÃO as altera —
285
+ * senão o `config/authkit.ts` deixa de ser auditável e seria preciso ler o
286
+ * `start/routes.ts` de cada app para saber o que está valendo. Mesma regra
287
+ * (e mesma derivação) de `defineConfig({ authMethods })`, que já fixa os
288
+ * métodos de login contra o runtime. Ver `deriveLockedRouteOptions`.
289
+ * A divergência NÃO é silenciosa: sai um `console.warn` nomeando a chave e
290
+ * a chave aparece em `AuthHostRouteMap.overriddenByConfig`.
291
+ *
292
+ * Devolve o {@link AuthHostRouteMap} resolvido — entregue-o ao frontend em vez
293
+ * de hardcodar `href`s.
294
+ */
295
+ export declare function registerAuthHost(router: Router, opts?: AuthHostOptions): AuthHostRouteMap;
296
+ /**
297
+ * Auto-montagem a partir de `config.routes` (R1). Chamada UMA vez pelo
298
+ * `boot()` do provider.
299
+ *
300
+ * O CAMINHO AUTOMÁTICO É O CAMINHO MANUAL: chama literalmente
301
+ * `registerAuthHost`, sem nenhuma segunda implementação. Toda a resolução
302
+ * (herança do config, precedência política × estrutural, mapa de retorno) é a
303
+ * mesma — duas implementações de um comportamento sempre divergem.
304
+ *
305
+ * Não recebe opções: os defaults ESTRUTURAIS vêm de `config.routes` pelo stash
306
+ * (`AuthHostRuntimeConfig.routes`), pelo mesmo caminho que uma chamada manual
307
+ * os leria.
308
+ *
309
+ * @internal
209
310
  */
210
- export declare function registerAuthHost(router: Router, opts?: AuthHostOptions): void;
311
+ export declare function autoMountAuthHost(router: Router): AuthHostRouteMap;