@adonis-agora/authkit-server 0.85.1 → 0.86.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.
@@ -29,6 +29,21 @@ export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promis
29
29
  * (o `adminGuard` 404a quando `config.admin.enabled` é false). Mantenha-os em
30
30
  * sincronia; os guards garantem que a divergência não vire um bypass.
31
31
  */
32
+ /**
33
+ * Rotas do fluxo de login que o host pode querer reforçar por conta própria
34
+ * (throttle mais apertado, WAF, log de auditoria, feature flag…).
35
+ *
36
+ * A lib continua aplicando o throttle padrão (`rateLimit`), mas estas são as
37
+ * rotas em que o host tem contexto para ir além — ex.: limitar `/identifier`
38
+ * por e-mail em vez de só por IP.
39
+ */
40
+ export type AuthHostRouteKey = 'interaction' | 'identifier' | 'login' | 'mfa' | 'mfaCustomBegin' | 'mfaCustomVerify' | 'passwordExpired' | 'passkeyOptions' | 'passkeyVerify' | 'magic' | 'magicConsume' | 'otpVerify' | 'consent' | 'switchIdentifier';
41
+ /**
42
+ * Middleware do AdonisJS aceito por `route.use` — função, import lazy, o nome de um
43
+ * middleware do router, ou a tupla `[middleware, options]`. Tipado frouxo de propósito:
44
+ * a lib não conhece os middlewares do host e não deve inferir a forma deles.
45
+ */
46
+ export type AuthHostMiddleware = unknown;
32
47
  export interface AuthHostOptions {
33
48
  /**
34
49
  * Onde o provider OIDC é montado. Deve casar com o final do issuer. OPCIONAL:
@@ -58,6 +73,26 @@ export interface AuthHostOptions {
58
73
  * NÃO espelhe o config aqui — omitir a chave já herda dele.
59
74
  */
60
75
  rateLimit?: RateLimitConfigInput;
76
+ /**
77
+ * Middleware EXTRA por rota do fluxo de login, aplicado DEPOIS do throttle da lib.
78
+ *
79
+ * É o escape hatch para quem quer política própria sem fork: a rota continua sendo
80
+ * da lib (handler, validação, OIDC), mas o host pendura o que quiser nela —
81
+ * `throttle` do AdonisJS, verificação de origem, auditoria:
82
+ *
83
+ * ```ts
84
+ * registerAuthHost(router, {
85
+ * extraMiddleware: {
86
+ * identifier: [throttle({ limit: 5, window: '1 minute' })],
87
+ * },
88
+ * })
89
+ * ```
90
+ *
91
+ * Não é opção de POLÍTICA (não é travada pelo `defineConfig`): é encanamento.
92
+ * Rodeu antes do throttle da lib? Não: a ordem é lib → host, então o seu middleware
93
+ * vê a requisição depois da barreira padrão.
94
+ */
95
+ extraMiddleware?: Partial<Record<AuthHostRouteKey, readonly AuthHostMiddleware[]>>;
61
96
  /**
62
97
  * Console admin opt-in (B6).
63
98
  *
@@ -272,6 +307,24 @@ export interface AuthHostRouteMap {
272
307
  } | null;
273
308
  /** Nomes das rotas nomeadas (as demais herdam o auto-naming do AdonisJS). */
274
309
  names: Record<string, string>;
310
+ /**
311
+ * Handles das rotas do fluxo de login (os `Route` do AdonisJS), por chave estável.
312
+ *
313
+ * Serve para o host pendurar middleware DEPOIS do registro (`route.use([...])`) ou
314
+ * inspecionar o que já está aplicado. Para o mesmo efeito de forma declarativa, use
315
+ * a opção `extraMiddleware` ao chamar `registerAuthHost`.
316
+ */
317
+ routes: Partial<Record<AuthHostRouteKey, ReturnType<Router['post']>>>;
318
+ /** Pattern de cada rota do fluxo (para montar href sem hardcodar). `:uid` incluído. */
319
+ urls: Record<AuthHostRouteKey, string>;
320
+ /**
321
+ * URL concreta de uma rota do fluxo, com o `:uid` já preenchido.
322
+ *
323
+ * ```ts
324
+ * map.interactionUrl('magic', uid) // '/auth/interaction/<uid>/magic'
325
+ * ```
326
+ */
327
+ interactionUrl: (key: AuthHostRouteKey, uid: string) => string;
275
328
  /** Ids dos métodos de sudo cujas rotas foram montadas, na ordem de montagem. */
276
329
  sudoMethods: string[];
277
330
  /**
@@ -389,6 +389,7 @@ export function registerAuthHost(router, opts = {}) {
389
389
  const withLogin = (route) => {
390
390
  if (throttles)
391
391
  route.use([throttles.login]);
392
+ return route;
392
393
  };
393
394
  const withIntrospection = (route) => {
394
395
  if (throttles)
@@ -406,6 +407,33 @@ export function registerAuthHost(router, opts = {}) {
406
407
  const withOtpLogin = (route) => {
407
408
  if (throttles)
408
409
  route.use([throttles.otpLogin]);
410
+ return route;
411
+ };
412
+ const hostRoutes = {};
413
+ const interactionUrls = {
414
+ interaction: '/auth/interaction/:uid',
415
+ identifier: '/auth/interaction/:uid/identifier',
416
+ login: '/auth/interaction/:uid/login',
417
+ mfa: '/auth/interaction/:uid/mfa',
418
+ mfaCustomBegin: '/auth/interaction/:uid/mfa/custom/:method/begin',
419
+ mfaCustomVerify: '/auth/interaction/:uid/mfa/custom/:method/verify',
420
+ passwordExpired: '/auth/interaction/:uid/password-expired',
421
+ passkeyOptions: '/auth/interaction/:uid/passkey/options',
422
+ passkeyVerify: '/auth/interaction/:uid/passkey/verify',
423
+ magic: '/auth/interaction/:uid/magic',
424
+ magicConsume: '/auth/interaction/:uid/magic',
425
+ otpVerify: '/auth/interaction/:uid/otp-verify',
426
+ consent: '/auth/interaction/:uid/consent',
427
+ switchIdentifier: '/auth/interaction/:uid/switch',
428
+ };
429
+ const keep = (key, route) => {
430
+ hostRoutes[key] = route;
431
+ const extraForRoute = opts.extraMiddleware?.[key];
432
+ if (extraForRoute && extraForRoute.length > 0) {
433
+ // `use` espera o MiddlewareList do AdonisJS; aqui é lista opaca do host.
434
+ route.use([...extraForRoute]);
435
+ }
436
+ return route;
409
437
  };
410
438
  // ─── Assets estáticos do host-kit (públicos, sem autenticação) ─────────────
411
439
  // Bundle do @simplewebauthn/browser servido pelo próprio app, no lugar do
@@ -452,38 +480,46 @@ export function registerAuthHost(router, opts = {}) {
452
480
  router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
453
481
  router.any(mount, [C.oidc]).as('authkit.oidc.root');
454
482
  // Interaction (login multi-step + consent + signup).
455
- router.get('/auth/interaction/:uid', [C.interaction, 'show']);
456
- router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier']);
457
- withLogin(router.post('/auth/interaction/:uid/login', [C.interaction, 'login']));
458
- withLogin(router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify']));
459
- const customMfaBeginRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
483
+ // Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
484
+ // recebe o `extraMiddleware` do host (depois do throttle da lib).
485
+ keep('interaction', router.get('/auth/interaction/:uid', [C.interaction, 'show']));
486
+ // `/identifier` é a ENTRADA do fluxo: recebe só o e-mail e abre/avança a interaction.
487
+ // Sem throttle, um anônimo cria estado de interaction sem limite (Redis) e o passo
488
+ // fica de fora do orçamento que o resto do login respeita. Bucket `login` (por IP),
489
+ // o mesmo do `/magic`: um login normal gasta 2 dos 10 pontos por minuto.
490
+ keep('identifier', withLogin(router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier'])));
491
+ withLogin(keep('login', router.post('/auth/interaction/:uid/login', [C.interaction, 'login'])));
492
+ withLogin(keep('mfa', router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify'])));
493
+ const customMfaBeginRoute = keep('mfaCustomBegin', router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
460
494
  C.interaction,
461
495
  'customMfaBegin',
462
- ]);
496
+ ]));
463
497
  withLogin(customMfaBeginRoute);
464
498
  customMfaBeginRoute.as('authkit.mfa.custom.begin');
465
- const customMfaVerifyRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
499
+ const customMfaVerifyRoute = keep('mfaCustomVerify', router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
466
500
  C.interaction,
467
501
  'customMfaVerify',
468
- ]);
502
+ ]));
469
503
  withLogin(customMfaVerifyRoute);
470
504
  customMfaVerifyRoute.as('authkit.mfa.custom.verify');
471
505
  // Troca de senha obrigatória quando a senha expirou (password expiration gate).
472
- withLogin(router.post('/auth/interaction/:uid/password-expired', [
506
+ withLogin(keep('passwordExpired', router.post('/auth/interaction/:uid/password-expired', [
473
507
  C.interaction,
474
508
  'changeExpiredPassword',
475
- ]));
509
+ ])));
476
510
  // Passkey como 2º fator alternativo no login (begin/finish; challenge na sessão).
477
- router.post('/auth/interaction/:uid/passkey/options', [C.interaction, 'passkeyOptions']);
478
- withLogin(router.post('/auth/interaction/:uid/passkey/verify', [C.interaction, 'passkeyVerify']));
511
+ // O `options` também cria estado (challenge) e é anônimo: mesmo bucket `login` do
512
+ // `/identifier` e do `/magic`. Um login normal gasta 1 ponto aqui quando usa passkey.
513
+ keep('passkeyOptions', withLogin(router.post('/auth/interaction/:uid/passkey/options', [C.interaction, 'passkeyOptions'])));
514
+ withLogin(keep('passkeyVerify', router.post('/auth/interaction/:uid/passkey/verify', [C.interaction, 'passkeyVerify'])));
479
515
  // Magic link (passwordless): POST emite (throttled), GET consome o token do link.
480
- withLogin(router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest']));
481
- router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']);
516
+ withLogin(keep('magic', router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest'])));
517
+ keep('magicConsume', router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']));
482
518
  // Login por OTP (código digitável): verifica o código no bucket dedicado
483
519
  // `authkit_otp_login` (por IP, mais apertado que o login).
484
- withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify']));
485
- router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']);
486
- router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']);
520
+ keep('otpVerify', withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify'])));
521
+ keep('consent', router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']));
522
+ keep('switchIdentifier', router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']));
487
523
  // OTP unlock: link enviado por e-mail quando o fator TOTP/recovery é travado.
488
524
  router.get('/auth/otp-unlock/:token', [C.interaction, 'otpUnlock']);
489
525
  router.get('/auth/interaction/:uid/signup', [C.registration, 'showSignup']);
@@ -1026,6 +1062,9 @@ export function registerAuthHost(router, opts = {}) {
1026
1062
  // inteiro, e não só metade (certo no servidor, errado na UI).
1027
1063
  return {
1028
1064
  mountPath: mount,
1065
+ routes: hostRoutes,
1066
+ urls: interactionUrls,
1067
+ interactionUrl: (key, uid) => interactionUrls[key].replace(':uid', encodeURIComponent(uid)),
1029
1068
  account: {
1030
1069
  prefix: accountPrefix(),
1031
1070
  api: apiBase,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.85.1",
3
+ "version": "0.86.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",