@adonis-agora/authkit-server 0.90.2 → 1.0.0-rc.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +3654 -0
  2. package/build/commands/clients_create.d.ts +1 -0
  3. package/build/commands/clients_create.js +15 -2
  4. package/build/commands/commands.json +7 -0
  5. package/build/host/ui/admin.html +1 -1
  6. package/build/host/views/account/apps.edge +8 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +5 -3
  9. package/build/index.js +4 -5
  10. package/build/providers/authkit_server_provider.js +8 -0
  11. package/build/src/accounts/lucid_account_store.d.ts +0 -18
  12. package/build/src/accounts/lucid_account_store.js +7 -48
  13. package/build/src/accounts/lucid_store/mfa.js +1 -1
  14. package/build/src/accounts/lucid_store/organizations.d.ts +1 -1
  15. package/build/src/accounts/lucid_store/password_hygiene.d.ts +2 -3
  16. package/build/src/accounts/lucid_store/password_hygiene.js +2 -3
  17. package/build/src/accounts/lucid_store/shared.d.ts +0 -7
  18. package/build/src/accounts/lucid_store/shared.js +0 -14
  19. package/build/src/accounts/lucid_stores.d.ts +1 -1
  20. package/build/src/adapters/redis_adapter.js +12 -2
  21. package/build/src/console_authorize.d.ts +56 -0
  22. package/build/src/console_authorize.js +85 -0
  23. package/build/src/define_config.d.ts +26 -14
  24. package/build/src/define_config.js +5 -3
  25. package/build/src/doctor/checks.d.ts +10 -5
  26. package/build/src/doctor/checks.js +52 -8
  27. package/build/src/host/account_api/account_api_controller.d.ts +2 -1
  28. package/build/src/host/account_api/account_api_controller.js +5 -11
  29. package/build/src/host/admin_api/api_clients_controller.d.ts +9 -3
  30. package/build/src/host/admin_api/dto.d.ts +3 -1
  31. package/build/src/host/admin_api/dto.js +3 -1
  32. package/build/src/host/admin_clients_service.d.ts +13 -5
  33. package/build/src/host/admin_clients_service.js +28 -6
  34. package/build/src/host/admin_console/admin_shell_controller.d.ts +16 -1
  35. package/build/src/host/admin_console/admin_shell_controller.js +21 -7
  36. package/build/src/host/admin_console/console_clients_controller.d.ts +21 -2
  37. package/build/src/host/admin_console/console_clients_controller.js +9 -0
  38. package/build/src/host/admin_console/console_impersonation_controller.js +3 -8
  39. package/build/src/host/admin_console/console_overview_controller.d.ts +2 -2
  40. package/build/src/host/admin_console/console_overview_controller.js +4 -2
  41. package/build/src/host/admin_console_warning.d.ts +8 -0
  42. package/build/src/host/admin_console_warning.js +18 -0
  43. package/build/src/host/admin_validators.d.ts +15 -11
  44. package/build/src/host/admin_validators.js +16 -9
  45. package/build/src/host/auth_host_config.d.ts +5 -0
  46. package/build/src/host/client_names.d.ts +9 -2
  47. package/build/src/host/client_names.js +11 -3
  48. package/build/src/host/consent_params.d.ts +7 -0
  49. package/build/src/host/consent_params.js +27 -0
  50. package/build/src/host/console_denied_page.d.ts +6 -0
  51. package/build/src/host/console_denied_page.js +18 -0
  52. package/build/src/host/console_sso.d.ts +24 -0
  53. package/build/src/host/console_sso.js +70 -0
  54. package/build/src/host/controllers/account_apps_controller.js +4 -3
  55. package/build/src/host/controllers/account_confirm_controller.d.ts +0 -7
  56. package/build/src/host/controllers/account_confirm_controller.js +0 -6
  57. package/build/src/host/controllers/account_orgs_controller.d.ts +0 -50
  58. package/build/src/host/controllers/account_orgs_controller.js +7 -113
  59. package/build/src/host/controllers/account_security_controller.js +7 -11
  60. package/build/src/host/controllers/account_session_controller.js +10 -1
  61. package/build/src/host/controllers/agent_consent_controller.js +23 -5
  62. package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
  63. package/build/src/host/controllers/console_sso_controller.js +61 -0
  64. package/build/src/host/controllers/interaction_controller.js +9 -3
  65. package/build/src/host/controllers/poppy_consent_controller.js +33 -7
  66. package/build/src/host/controllers/registration_controller.js +7 -2
  67. package/build/src/host/i18n.d.ts +0 -4
  68. package/build/src/host/i18n.js +0 -6
  69. package/build/src/host/impersonation.d.ts +7 -15
  70. package/build/src/host/impersonation.js +7 -16
  71. package/build/src/host/interaction_recovery.js +4 -1
  72. package/build/src/host/login_attempt.js +5 -1
  73. package/build/src/host/middleware/account_auth.js +6 -0
  74. package/build/src/host/rate_limit.js +5 -1
  75. package/build/src/host/register_auth_host.d.ts +8 -0
  76. package/build/src/host/register_auth_host.js +116 -11
  77. package/build/src/host/renderers/inertia_renderer.d.ts +9 -21
  78. package/build/src/host/renderers/inertia_renderer.js +5 -6
  79. package/build/src/host/screen_props.d.ts +309 -0
  80. package/build/src/host/screen_props.js +11 -0
  81. package/build/src/host/sudo_mode.d.ts +5 -11
  82. package/build/src/host/sudo_mode.js +5 -11
  83. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +67 -0
  84. package/build/src/host/ui-dist/index.html +1 -1
  85. package/build/src/password/password_manager.js +5 -0
  86. package/build/src/provider/build_provider.js +6 -34
  87. package/build/src/provider/oidc_service.d.ts +0 -2
  88. package/build/src/provider/oidc_service.js +1 -12
  89. package/build/src/provider/token_exchange.d.ts +1 -3
  90. package/build/src/provider/token_exchange.js +1 -1
  91. package/build/stubs/config/authkit_react.stub +3 -3
  92. package/package.json +12 -11
  93. package/skills/authkit-interactions/SKILL.md +32 -5
  94. package/stubs/config/authkit_react.stub +3 -3
  95. package/build/src/commands/import_clients.d.ts +0 -40
  96. package/build/src/commands/import_clients.js +0 -81
  97. package/build/src/host/ui-dist/assets/index-DUVBDAyh.js +0 -67
  98. package/build/src/mixins/with_mfa.d.ts +0 -35
  99. package/build/src/mixins/with_mfa.js +0 -21
@@ -0,0 +1,24 @@
1
+ /** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
2
+ export declare const CONSOLE_SSO_CLIENT_ID = "authkit-console";
3
+ /** Rota que inicia o login do console pelo IdP. */
4
+ export declare const CONSOLE_SSO_START_PATH = "/auth/console/sso";
5
+ /** `redirect_uri` do client interno: valida o `state` e abre o console. */
6
+ export declare const CONSOLE_SSO_CALLBACK_PATH = "/auth/console/callback";
7
+ /** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
8
+ export declare const CONSOLE_SSO_STATE_KEY = "authkit_console_sso";
9
+ /** O que fica na sessão entre o início e o callback. */
10
+ export interface ConsoleSsoState {
11
+ state: string;
12
+ returnTo: string | null;
13
+ }
14
+ /** Metadata do client interno para o `clients` estático do oidc-provider. */
15
+ export declare function consoleSsoClientMetadata(issuer: string): Record<string, unknown>;
16
+ /** Novo `state` opaco para um login do console. */
17
+ export declare function newConsoleSsoState(): string;
18
+ /**
19
+ * Path do authorize do provider. O issuer termina no `mountPath` (garantido por
20
+ * `registerAuthHost`), então o mount é o path do issuer.
21
+ */
22
+ export declare function authorizationPath(issuer: string, provider?: any): string;
23
+ /** URL do login do console pelo IdP, preservando o destino como `return_to`. */
24
+ export declare function consoleSsoUrl(returnTo?: string | null): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Login do console pelo IdP (SSO), o jeito padrão de entrar em `/account/*` e no
3
+ * console admin.
4
+ *
5
+ * O console é tratado como mais um app do IdP, como o Keycloak faz com o
6
+ * `account-console` e o `security-admin-console`: sem sessão, o visitante vai
7
+ * para a tela de login do IdP (a mesma interaction de qualquer client: senha,
8
+ * magic link, OTP, passkey, social), volta, e a ponte de
9
+ * `idp_session_bridge.ts` abre o console para a conta da sessão do IdP.
10
+ *
11
+ * Para isso existe um client interno, `authkit-console`, injetado como client
12
+ * ESTÁTICO do provider (não fica no adapter, então não aparece na lista de
13
+ * clients do console nem pode ser editado):
14
+ *
15
+ * - `response_types: ['none']`: o authorize não emite code nem token. O client
16
+ * só usa o IdP para autenticar; o que abre o console é a sessão do IdP que o
17
+ * login criou, lida pela ponte. Não há segredo, PKCE ou troca de code.
18
+ * - o consent é concedido automaticamente (é a própria lib).
19
+ * - a única `redirect_uri` é `/auth/console/callback`, no MESMO host do issuer.
20
+ *
21
+ * Só existe com `accountSession.acceptIdpSession` ligado (o default).
22
+ */
23
+ import { randomBytes } from 'node:crypto';
24
+ /** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
25
+ export const CONSOLE_SSO_CLIENT_ID = 'authkit-console';
26
+ /** Rota que inicia o login do console pelo IdP. */
27
+ export const CONSOLE_SSO_START_PATH = '/auth/console/sso';
28
+ /** `redirect_uri` do client interno: valida o `state` e abre o console. */
29
+ export const CONSOLE_SSO_CALLBACK_PATH = '/auth/console/callback';
30
+ /** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
31
+ export const CONSOLE_SSO_STATE_KEY = 'authkit_console_sso';
32
+ /** Metadata do client interno para o `clients` estático do oidc-provider. */
33
+ export function consoleSsoClientMetadata(issuer) {
34
+ const origin = new URL(issuer).origin;
35
+ return {
36
+ client_id: CONSOLE_SSO_CLIENT_ID,
37
+ client_name: 'Account console',
38
+ application_type: 'web',
39
+ token_endpoint_auth_method: 'none',
40
+ response_types: ['none'],
41
+ grant_types: [],
42
+ redirect_uris: [`${origin}${CONSOLE_SSO_CALLBACK_PATH}`],
43
+ };
44
+ }
45
+ /** Novo `state` opaco para um login do console. */
46
+ export function newConsoleSsoState() {
47
+ return randomBytes(24).toString('base64url');
48
+ }
49
+ /**
50
+ * Path do authorize do provider. O issuer termina no `mountPath` (garantido por
51
+ * `registerAuthHost`), então o mount é o path do issuer.
52
+ */
53
+ export function authorizationPath(issuer, provider) {
54
+ const mount = new URL(issuer).pathname.replace(/\/+$/, '');
55
+ if (provider && typeof provider.pathFor === 'function') {
56
+ try {
57
+ return provider.pathFor('authorization', { mountPath: mount });
58
+ }
59
+ catch {
60
+ // cai no default do oidc-provider abaixo
61
+ }
62
+ }
63
+ return `${mount}/auth`;
64
+ }
65
+ /** URL do login do console pelo IdP, preservando o destino como `return_to`. */
66
+ export function consoleSsoUrl(returnTo) {
67
+ if (!returnTo)
68
+ return CONSOLE_SSO_START_PATH;
69
+ return `${CONSOLE_SSO_START_PATH}?return_to=${encodeURIComponent(returnTo)}`;
70
+ }
@@ -4,7 +4,7 @@ import { personalAgentsFor } from '../../agents/runtime.js';
4
4
  import { accountPath } from '../account_paths.js';
5
5
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
6
6
  import { AdminSessionsService } from '../admin_sessions_service.js';
7
- import { clientDisplayNames } from '../client_names.js';
7
+ import { clientDisplayInfo } from '../client_names.js';
8
8
  /**
9
9
  * Self-service de consentimento ("apps com acesso") no console de conta. Lista os
10
10
  * Grants da própria conta agrupados por client (resolvendo o nome do client da
@@ -23,8 +23,8 @@ export default class AccountAppsController {
23
23
  const supported = sessions.canList;
24
24
  const grantList = supported ? await sessions.listGrants(accountId) : [];
25
25
  // O nome que o client registrou; sem ele, o clientId.
26
- const names = await clientDisplayNames(service, grantList.map((g) => g.clientId).filter((id) => !!id));
27
- const nameOf = (clientId) => clientId ? (names.get(clientId) ?? clientId) : '';
26
+ const info = await clientDisplayInfo(service, grantList.map((g) => g.clientId).filter((id) => !!id));
27
+ const nameOf = (clientId) => clientId ? (info.get(clientId)?.name ?? clientId) : '';
28
28
  const revoked = ctx.session.flashMessages.get('appRevoked');
29
29
  // Personal agents com delegação ativa (só quando a feature está ligada).
30
30
  const agentsRuntime = await personalAgentsFor(ctx);
@@ -58,6 +58,7 @@ export default class AccountAppsController {
58
58
  .map((g) => ({
59
59
  clientId: g.clientId,
60
60
  name: nameOf(g.clientId),
61
+ logoUrl: info.get(g.clientId)?.logoUrl ?? null,
61
62
  accessTokens: g.accessTokens,
62
63
  refreshTokens: g.refreshTokens,
63
64
  })),
@@ -9,13 +9,6 @@
9
9
  */
10
10
  import '../augmentations.js';
11
11
  import type { HttpContext } from '@adonisjs/core/http';
12
- import { sudoContextFrom } from '../sudo/runtime.js';
13
- /**
14
- * Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
15
- * `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
16
- * antigo segue valendo para quem já o importava.
17
- */
18
- export { sudoContextFrom };
19
12
  export default class AccountConfirmController {
20
13
  show(ctx: HttpContext): Promise<unknown>;
21
14
  }
@@ -9,12 +9,6 @@
9
9
  */
10
10
  import '../augmentations.js';
11
11
  import { configuredSudoMethods, isSudoMethodMounted, LAST_METHOD_SESSION_KEY, resolveAvailableMethods, sudoContextFrom, } from '../sudo/runtime.js';
12
- /**
13
- * Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
14
- * `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
15
- * antigo segue valendo para quem já o importava.
16
- */
17
- export { sudoContextFrom };
18
12
  export default class AccountConfirmController {
19
13
  async show(ctx) {
20
14
  const c = await sudoContextFrom(ctx);
@@ -27,54 +27,4 @@ export default class AccountOrgsController {
27
27
  removeMember(ctx: HttpContext): Promise<void>;
28
28
  /** POST /account/orgs/:id/invitations/:invId/revoke */
29
29
  revokeInvitation(ctx: HttpContext): Promise<void>;
30
- /**
31
- * GET /account/orgs/json — lista as orgs do usuário logado com papel + se ativa.
32
- * Usado pelo hook `useOrganizations()`.
33
- */
34
- listJson(ctx: HttpContext): Promise<void | {
35
- supported: boolean;
36
- activeOrgId: any;
37
- orgs: {
38
- id: string;
39
- name: string;
40
- slug: string;
41
- logoUrl: string | null;
42
- role: string;
43
- isActive: boolean;
44
- }[];
45
- }>;
46
- /**
47
- * GET /account/orgs/:id/json — detalhes da org ativa (para uso pelo hook `useOrganization()`).
48
- * Só retorna se o usuário é membro.
49
- */
50
- showJson(ctx: HttpContext): Promise<void | {
51
- id: string;
52
- name: string;
53
- slug: string;
54
- logoUrl: string | null;
55
- role: string;
56
- canManage: boolean;
57
- members: {
58
- accountId: string;
59
- email: string | null;
60
- role: string;
61
- joinedAt: string;
62
- }[];
63
- }>;
64
- /**
65
- * GET /account/orgs/invitations/json — lista convites pendentes pro e-mail do usuário logado.
66
- * Usado pelo hook `useOrgInvitations()`.
67
- */
68
- listInvitationsJson(ctx: HttpContext): Promise<void | {
69
- invitations: {
70
- id: string;
71
- organizationId: string;
72
- orgName: string;
73
- orgSlug: string;
74
- email: string;
75
- role: string;
76
- expiresAt: string;
77
- createdAt: string;
78
- }[];
79
- }>;
80
30
  }
@@ -27,12 +27,10 @@ export default class AccountOrgsController {
27
27
  const service = await ctx.containerResolver.make('authkit.server');
28
28
  const cfg = service.config;
29
29
  const store = cfg.accountStore;
30
- const messages = cfg.messages;
31
30
  if (!supportsOrganizations(store)) {
32
31
  return cfg.render
33
32
  ? cfg.render(ctx, 'account/orgs', {
34
33
  supported: false,
35
- messages,
36
34
  csrfToken: ctx.request.csrfToken,
37
35
  })
38
36
  : response.notFound();
@@ -64,10 +62,11 @@ export default class AccountOrgsController {
64
62
  pendingInvitations: invitationsWithOrg,
65
63
  allowSelfCreate: policy.allowSelfCreate,
66
64
  availableRoles: policy.roles,
67
- messages,
68
65
  csrfToken: ctx.request.csrfToken,
69
66
  };
70
- return cfg.render ? cfg.render(ctx, 'account/orgs', props) : response.notFound();
67
+ return cfg.render
68
+ ? cfg.render(ctx, 'account/orgs', props)
69
+ : response.notFound();
71
70
  }
72
71
  /** POST /account/orgs — criar nova org (requer allowSelfCreate). */
73
72
  async store(ctx) {
@@ -265,11 +264,13 @@ export default class AccountOrgsController {
265
264
  const props = {
266
265
  invitation,
267
266
  token: params.token,
268
- messages: cfg.messages,
269
267
  csrfToken: ctx.request.csrfToken,
270
268
  };
271
269
  return cfg.render
272
- ? cfg.render(ctx, 'account/orgs', { ...props, subview: 'accept-invitation' })
270
+ ? cfg.render(ctx, 'account/orgs', {
271
+ ...props,
272
+ subview: 'accept-invitation',
273
+ })
273
274
  : response.notFound();
274
275
  }
275
276
  /** POST /account/orgs/invitations/:token/accept — processa aceite. */
@@ -351,111 +352,4 @@ export default class AccountOrgsController {
351
352
  });
352
353
  return response.redirect(accountPath('orgs'));
353
354
  }
354
- // ──────────────────────────────────────────────────────────────────────────
355
- // JSON endpoints — consumed by @adonis-agora/authkit-react hooks.
356
- // ──────────────────────────────────────────────────────────────────────────
357
- /**
358
- * GET /account/orgs/json — lista as orgs do usuário logado com papel + se ativa.
359
- * Usado pelo hook `useOrganizations()`.
360
- */
361
- async listJson(ctx) {
362
- const { session, response, request } = ctx;
363
- const service = await ctx.containerResolver.make('authkit.server');
364
- const cfg = service.config;
365
- const store = cfg.accountStore;
366
- if (!supportsOrganizations(store))
367
- return { supported: false, orgs: [], activeOrgId: null };
368
- const accountId = session.get(ACCOUNT_SESSION_KEY);
369
- if (!accountId)
370
- return response.unauthorized({ message: 'Not authenticated.' });
371
- const orgs = await store.listOrgsForAccount(accountId);
372
- const activeOrgRaw = request.cookie(ACTIVE_ORG_COOKIE);
373
- const activeOrgId = activeOrgRaw ? activeOrgRaw.split('\t')[0] : null;
374
- return {
375
- supported: true,
376
- activeOrgId,
377
- orgs: orgs.map((org) => ({
378
- id: org.id,
379
- name: org.name,
380
- slug: org.slug,
381
- logoUrl: org.logoUrl ?? null,
382
- role: org.role,
383
- isActive: org.id === activeOrgId,
384
- })),
385
- };
386
- }
387
- /**
388
- * GET /account/orgs/:id/json — detalhes da org ativa (para uso pelo hook `useOrganization()`).
389
- * Só retorna se o usuário é membro.
390
- */
391
- async showJson(ctx) {
392
- const { session, response, params } = ctx;
393
- const service = await ctx.containerResolver.make('authkit.server');
394
- const cfg = service.config;
395
- const store = cfg.accountStore;
396
- if (!supportsOrganizations(store))
397
- return response.notFound({ message: 'Not supported.' });
398
- const accountId = session.get(ACCOUNT_SESSION_KEY);
399
- if (!accountId)
400
- return response.unauthorized({ message: 'Not authenticated.' });
401
- const membership = await store.getOrgMembership(params.id, accountId);
402
- if (!membership)
403
- return response.notFound({ message: 'Organization not found or not a member.' });
404
- const org = await store.findOrgById(params.id);
405
- if (!org)
406
- return response.notFound({ message: 'Organization not found.' });
407
- const canManage = membership.role === 'owner' || membership.role === 'admin';
408
- const members = canManage ? await store.listOrgMembers(params.id) : [];
409
- const enrichedMembers = await Promise.all(members.map(async (m) => {
410
- const account = await store.findById(m.accountId);
411
- return {
412
- accountId: m.accountId,
413
- email: account?.email ?? null,
414
- role: m.role,
415
- joinedAt: m.joinedAt,
416
- };
417
- }));
418
- return {
419
- id: org.id,
420
- name: org.name,
421
- slug: org.slug,
422
- logoUrl: org.logoUrl ?? null,
423
- role: membership.role,
424
- canManage,
425
- members: enrichedMembers,
426
- };
427
- }
428
- /**
429
- * GET /account/orgs/invitations/json — lista convites pendentes pro e-mail do usuário logado.
430
- * Usado pelo hook `useOrgInvitations()`.
431
- */
432
- async listInvitationsJson(ctx) {
433
- const { session, response } = ctx;
434
- const service = await ctx.containerResolver.make('authkit.server');
435
- const cfg = service.config;
436
- const store = cfg.accountStore;
437
- if (!supportsOrganizations(store))
438
- return { invitations: [] };
439
- const accountId = session.get(ACCOUNT_SESSION_KEY);
440
- if (!accountId)
441
- return response.unauthorized({ message: 'Not authenticated.' });
442
- const account = await store.findById(accountId);
443
- if (!account)
444
- return response.unauthorized({ message: 'Not authenticated.' });
445
- const invitations = await store.listPendingInvitationsForEmail(account.email);
446
- const enriched = await Promise.all(invitations.map(async (inv) => {
447
- const org = await store.findOrgById(inv.organizationId);
448
- return {
449
- id: inv.id,
450
- organizationId: inv.organizationId,
451
- orgName: org?.name ?? inv.organizationId,
452
- orgSlug: org?.slug ?? inv.organizationId,
453
- email: inv.email,
454
- role: inv.role,
455
- expiresAt: inv.expiresAt,
456
- createdAt: inv.createdAt,
457
- };
458
- }));
459
- return { invitations: enriched };
460
- }
461
355
  }
@@ -426,7 +426,7 @@ export default class AccountSecurityController {
426
426
  });
427
427
  const origin = authkitOrigin(cfg);
428
428
  const confirmUrl = `${origin}${accountPath('emailConfirm')}?token=${encodeURIComponent(issued.token)}`;
429
- // 1. Link de confirmação → NOVO e-mail (hook onEmailChangeConfirm > onEmailVerification > default).
429
+ // 1. Link de confirmação → NOVO e-mail (hook onEmailChangeConfirm > default).
430
430
  if (cfg.mail?.onEmailChangeConfirm) {
431
431
  await cfg.mail.onEmailChangeConfirm({
432
432
  email: newEmail,
@@ -435,14 +435,6 @@ export default class AccountSecurityController {
435
435
  oldEmail: account?.email ?? '',
436
436
  });
437
437
  }
438
- else if (cfg.mail?.onEmailVerification) {
439
- // Retrocompat: hook legado onEmailVerification (ainda funciona como override).
440
- await cfg.mail.onEmailVerification({
441
- email: newEmail,
442
- verifyUrl: confirmUrl,
443
- token: issued.token,
444
- });
445
- }
446
438
  else {
447
439
  await sendEmailChangeConfirmationEmail(ctx, {
448
440
  email: newEmail,
@@ -513,7 +505,9 @@ export default class AccountSecurityController {
513
505
  const store = cfg.accountStore;
514
506
  const render = cfg.render;
515
507
  if (!supportsAccountSecurity(store)) {
516
- return render(ctx, 'account/email-confirmed', { ok: false });
508
+ return render(ctx, 'account/email-confirmed', {
509
+ ok: false,
510
+ });
517
511
  }
518
512
  const token = ctx.request.qs().token ?? '';
519
513
  const result = await store.confirmEmailChange(token);
@@ -551,6 +545,8 @@ export default class AccountSecurityController {
551
545
  metadata: { oldEmail: result.oldEmail, newEmail: result.newEmail },
552
546
  }, cfg.mail, cfg.audit, cfg);
553
547
  }
554
- return render(ctx, 'account/email-confirmed', { ok: result.ok });
548
+ return render(ctx, 'account/email-confirmed', {
549
+ ok: result.ok,
550
+ });
555
551
  }
556
552
  }
@@ -3,6 +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 { consoleSsoUrl } from '../console_sso.js';
6
7
  import { normalizeEmailIdentifier } from '../email_identifier.js';
7
8
  import { translate } from '../i18n.js';
8
9
  import { endBridgedIdpSession } from '../idp_session_bridge.js';
@@ -49,8 +50,16 @@ export default class AccountSessionController {
49
50
  return ctx.response.redirect(accountHome(cfg));
50
51
  }
51
52
  // Lê e valida o return_to da query-string — descarta valores inválidos (open-redirect).
52
- const rawReturnTo = ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to');
53
+ const qs = (ctx.request.qs?.() ?? {});
54
+ const rawReturnTo = qs.return_to ?? ctx.request.input?.('return_to');
53
55
  const returnTo = validateReturnTo(rawReturnTo);
56
+ // SSO (default): o console entra pelo login do IdP, a mesma tela de qualquer
57
+ // app. O formulário de senha daqui fica como porta alternativa
58
+ // (`?method=password`), e só quando a senha não foi desligada no config.
59
+ const passwordForm = qs.method === 'password' && cfg.authMethods?.password !== false;
60
+ if (cfg.accountSession?.acceptIdpSession === true && !passwordForm) {
61
+ return redirectExact(ctx.response, consoleSsoUrl(returnTo));
62
+ }
54
63
  const props = {
55
64
  csrfToken: ctx.request.csrfToken,
56
65
  returnTo,
@@ -51,14 +51,20 @@ export default class AgentConsentController {
51
51
  error: null,
52
52
  };
53
53
  if (impersonationState(ctx).active) {
54
- return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.impersonating' });
54
+ return render(ctx, 'agents/consent', {
55
+ ...base,
56
+ error: 'agents.consent.impersonating',
57
+ });
55
58
  }
56
59
  const input = ctx.request.input('user_code');
57
60
  if (!input)
58
61
  return render(ctx, 'agents/consent', base);
59
62
  const pending = await runtime.delegation.pendingRequest(input);
60
63
  if (!pending) {
61
- return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.invalid_code' });
64
+ return render(ctx, 'agents/consent', {
65
+ ...base,
66
+ error: 'agents.consent.invalid_code',
67
+ });
62
68
  }
63
69
  const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
64
70
  const account = await service.config.accountStore.findById(accountId);
@@ -96,7 +102,11 @@ export default class AgentConsentController {
96
102
  const userCode = typeof rawCode === 'string' ? rawCode : '';
97
103
  const pending = await runtime.delegation.pendingRequest(userCode);
98
104
  if (!pending) {
99
- return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
105
+ return render(ctx, 'agents/done', {
106
+ status: 'expired',
107
+ agentName: null,
108
+ scopes: [],
109
+ });
100
110
  }
101
111
  const name = await agentName(runtime, pending.clientId);
102
112
  const raw = ctx.request.input('scope');
@@ -108,7 +118,11 @@ export default class AgentConsentController {
108
118
  // Negou ou desmarcou tudo. Se nem negar deu, o pedido mudou de estado no
109
119
  // meio do caminho (aprovado em outra aba, expirou).
110
120
  if (!(await runtime.delegation.deny({ userCode, accountId }))) {
111
- return render(ctx, 'agents/done', { status: 'expired', agentName: name, scopes: [] });
121
+ return render(ctx, 'agents/done', {
122
+ status: 'expired',
123
+ agentName: name,
124
+ scopes: [],
125
+ });
112
126
  }
113
127
  await cfg.audit?.record({
114
128
  type: 'agent.delegation_denied',
@@ -117,7 +131,11 @@ export default class AgentConsentController {
117
131
  ip: ctx.request.ip?.() ?? null,
118
132
  metadata: { requested: pending.scopes.map((s) => s.id) },
119
133
  });
120
- return render(ctx, 'agents/done', { status: 'denied', agentName: name, scopes: [] });
134
+ return render(ctx, 'agents/done', {
135
+ status: 'denied',
136
+ agentName: name,
137
+ scopes: [],
138
+ });
121
139
  }
122
140
  await cfg.audit?.record({
123
141
  type: 'agent.delegation_approved',
@@ -0,0 +1,12 @@
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ /**
4
+ * Login do console pelo IdP (ver `console_sso.ts`).
5
+ *
6
+ * GET /auth/console/sso?return_to=… → authorize do client interno
7
+ * GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
8
+ */
9
+ export default class ConsoleSsoController {
10
+ start(ctx: HttpContext): Promise<unknown>;
11
+ callback(ctx: HttpContext): Promise<unknown>;
12
+ }
@@ -0,0 +1,61 @@
1
+ import '../augmentations.js';
2
+ import { accountHome } from '../account_home.js';
3
+ import { getAccountLoginUrl } from '../account_login_url.js';
4
+ import { authorizationPath, CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_CLIENT_ID, CONSOLE_SSO_STATE_KEY, newConsoleSsoState, } from '../console_sso.js';
5
+ import { ensureConsoleSession } from '../idp_session_bridge.js';
6
+ import { redirectExact } from '../redirect_exact.js';
7
+ import { validateReturnTo } from './account_session_controller.js';
8
+ /**
9
+ * Login do console pelo IdP (ver `console_sso.ts`).
10
+ *
11
+ * GET /auth/console/sso?return_to=… → authorize do client interno
12
+ * GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
13
+ */
14
+ export default class ConsoleSsoController {
15
+ async start(ctx) {
16
+ const service = await ctx.containerResolver.make('authkit.server');
17
+ const cfg = service.config;
18
+ const returnTo = validateReturnTo(ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to'));
19
+ // SSO desligado (`acceptIdpSession: false`): o console só tem o login próprio.
20
+ if (cfg.accountSession?.acceptIdpSession !== true) {
21
+ const login = getAccountLoginUrl();
22
+ const sep = login.includes('?') ? '&' : '?';
23
+ return redirectExact(ctx.response, returnTo ? `${login}${sep}return_to=${encodeURIComponent(returnTo)}` : login);
24
+ }
25
+ // Já tem sessão (de console ou do IdP): não precisa passar pelo IdP.
26
+ if (await ensureConsoleSession(ctx)) {
27
+ return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
28
+ }
29
+ const state = newConsoleSsoState();
30
+ ctx.session.put(CONSOLE_SSO_STATE_KEY, { state, returnTo });
31
+ const origin = new URL(cfg.issuer).origin;
32
+ const query = new URLSearchParams({
33
+ client_id: CONSOLE_SSO_CLIENT_ID,
34
+ response_type: 'none',
35
+ scope: 'openid',
36
+ redirect_uri: `${origin}${CONSOLE_SSO_CALLBACK_PATH}`,
37
+ state,
38
+ });
39
+ return redirectExact(ctx.response, `${authorizationPath(cfg.issuer, service.provider)}?${query.toString()}`);
40
+ }
41
+ async callback(ctx) {
42
+ const service = await ctx.containerResolver.make('authkit.server');
43
+ const cfg = service.config;
44
+ const pending = ctx.session.get(CONSOLE_SSO_STATE_KEY);
45
+ ctx.session.forget(CONSOLE_SSO_STATE_KEY);
46
+ const qs = (ctx.request.qs?.() ?? {});
47
+ // `state` amarra o callback ao navegador que iniciou o login (anti-CSRF de login).
48
+ if (!pending || typeof qs.state !== 'string' || qs.state !== pending.state) {
49
+ return ctx.response.status(400).send('Invalid or expired sign-in request.');
50
+ }
51
+ // O IdP recusou (o usuário cancelou, conta desabilitada, …): sem console.
52
+ // Vai para a raiz, e não de volta ao console, para não entrar em loop.
53
+ if (typeof qs.error === 'string') {
54
+ return redirectExact(ctx.response, '/');
55
+ }
56
+ if (!(await ensureConsoleSession(ctx))) {
57
+ return redirectExact(ctx.response, '/');
58
+ }
59
+ return redirectExact(ctx.response, pending.returnTo ?? accountHome(cfg));
60
+ }
61
+ }
@@ -4,6 +4,8 @@ import { resolveAccountRoles } from '../account_roles.js';
4
4
  import { AdminSessionsService } from '../admin_sessions_service.js';
5
5
  import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
6
6
  import { brandFor, isFirstParty } from '../branding.js';
7
+ import { consentRequestParams } from '../consent_params.js';
8
+ import { CONSOLE_SSO_CLIENT_ID } from '../console_sso.js';
7
9
  import { isCustomLoginMethodId } from '../custom_login.js';
8
10
  import { completeCustomLoginSession } from '../custom_login_completion.js';
9
11
  import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializeMfaFlow, recordVerifiedMfaFactor, resolveCustomMfaMethods, verifyCustomMfa, } from '../custom_mfa.js';
@@ -253,7 +255,8 @@ export default class AuthInteractionController {
253
255
  }
254
256
  }
255
257
  if (details.prompt.name === 'consent' &&
256
- isFirstParty(cfg.branding, details.params.client_id)) {
258
+ (details.params.client_id === CONSOLE_SSO_CLIENT_ID ||
259
+ isFirstParty(cfg.branding, details.params.client_id))) {
257
260
  // Clients first-party: auto-concede o consent (pula a tela de consent).
258
261
  // interactions.consent monta o Grant + interactionFinished e escreve o
259
262
  // redirect de volta para o client — opera via provider.interactionDetails,
@@ -264,7 +267,7 @@ export default class AuthInteractionController {
264
267
  // Consent or other prompts — unchanged
265
268
  return render(ctx, 'consent', {
266
269
  uid: details.uid,
267
- params: details.params,
270
+ params: consentRequestParams(details.params),
268
271
  csrfToken: ctx.request.csrfToken,
269
272
  brand,
270
273
  ...(await consentSubject(service, details, cfg.messages)),
@@ -1950,7 +1953,10 @@ export default class AuthInteractionController {
1950
1953
  ip,
1951
1954
  metadata: { reason: 'token_expired' },
1952
1955
  });
1953
- return render(ctx, 'otp-unlock', { ok: false, expired: true });
1956
+ return render(ctx, 'otp-unlock', {
1957
+ ok: false,
1958
+ expired: true,
1959
+ });
1954
1960
  }
1955
1961
  // Token válido: zera o lock OTP + limpa o token do DB.
1956
1962
  const runtimeForOtp = await resolveRuntimeSettingsOrNoop(ctx);