@adonis-agora/authkit-server 0.75.0 → 0.77.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 (101) hide show
  1. package/README.md +148 -0
  2. package/build/host/views/account/apps.edge +30 -0
  3. package/build/host/views/agents/consent.edge +60 -0
  4. package/build/host/views/agents/done.edge +24 -0
  5. package/build/host/views/consent.edge +9 -1
  6. package/build/host/views/mfa-challenge.edge +42 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +18 -1
  9. package/build/index.js +9 -1
  10. package/build/providers/authkit_server_provider.js +75 -50
  11. package/build/src/accounts/account_store.d.ts +2 -0
  12. package/build/src/accounts/lucid_account_store.js +2 -0
  13. package/build/src/adapters/adapter_contract.d.ts +2 -0
  14. package/build/src/adapters/database_adapter.d.ts +1 -0
  15. package/build/src/adapters/database_adapter.js +26 -0
  16. package/build/src/adapters/redis_adapter.d.ts +1 -0
  17. package/build/src/adapters/redis_adapter.js +30 -0
  18. package/build/src/agents/agent_identity.d.ts +33 -0
  19. package/build/src/agents/agent_identity.js +156 -0
  20. package/build/src/agents/config.d.ts +99 -0
  21. package/build/src/agents/config.js +101 -0
  22. package/build/src/agents/delegation_service.d.ts +154 -0
  23. package/build/src/agents/delegation_service.js +394 -0
  24. package/build/src/agents/delegation_store.d.ts +93 -0
  25. package/build/src/agents/delegation_store.js +222 -0
  26. package/build/src/agents/middleware.d.ts +73 -0
  27. package/build/src/agents/middleware.js +113 -0
  28. package/build/src/agents/protocol.d.ts +62 -0
  29. package/build/src/agents/protocol.js +51 -0
  30. package/build/src/agents/runtime.d.ts +36 -0
  31. package/build/src/agents/runtime.js +65 -0
  32. package/build/src/agents/signer.d.ts +34 -0
  33. package/build/src/agents/signer.js +70 -0
  34. package/build/src/audit/audit_sink.d.ts +1 -1
  35. package/build/src/audit/audit_sink.js +5 -0
  36. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  37. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  38. package/build/src/define_config.d.ts +50 -3
  39. package/build/src/define_config.js +42 -1
  40. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  41. package/build/src/host/account_api/account_api_controller.js +40 -0
  42. package/build/src/host/admin_api/dto.d.ts +1 -1
  43. package/build/src/host/admin_sessions_service.d.ts +2 -0
  44. package/build/src/host/admin_sessions_service.js +18 -0
  45. package/build/src/host/auth_host_config.d.ts +4 -0
  46. package/build/src/host/client_names.d.ts +11 -0
  47. package/build/src/host/client_names.js +12 -0
  48. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  49. package/build/src/host/controllers/account_apps_controller.js +39 -3
  50. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  51. package/build/src/host/controllers/account_security_controller.js +8 -4
  52. package/build/src/host/controllers/account_session_controller.js +2 -1
  53. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  54. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  55. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  56. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  57. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  58. package/build/src/host/controllers/interaction_controller.js +380 -24
  59. package/build/src/host/csrf.d.ts +8 -18
  60. package/build/src/host/csrf.js +28 -2
  61. package/build/src/host/custom_login.d.ts +36 -0
  62. package/build/src/host/custom_login.js +76 -0
  63. package/build/src/host/custom_login_completion.d.ts +4 -0
  64. package/build/src/host/custom_login_completion.js +33 -0
  65. package/build/src/host/custom_mfa.d.ts +77 -0
  66. package/build/src/host/custom_mfa.js +268 -0
  67. package/build/src/host/i18n.d.ts +58 -0
  68. package/build/src/host/i18n.js +64 -6
  69. package/build/src/host/idp_session_bridge.d.ts +5 -3
  70. package/build/src/host/idp_session_bridge.js +5 -3
  71. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  72. package/build/src/host/oidc_rp_guard.js +15 -2
  73. package/build/src/host/persistent_rp_session.d.ts +8 -0
  74. package/build/src/host/persistent_rp_session.js +74 -0
  75. package/build/src/host/redirect_exact.d.ts +12 -0
  76. package/build/src/host/redirect_exact.js +17 -0
  77. package/build/src/host/register_auth_host.js +60 -7
  78. package/build/src/host/request_url.d.ts +10 -0
  79. package/build/src/host/request_url.js +17 -0
  80. package/build/src/host/sudo/methods/magic_link.js +2 -1
  81. package/build/src/host/sudo/runtime.js +3 -2
  82. package/build/src/host/sudo_mode.js +4 -4
  83. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  84. package/build/src/host/whatsapp_code_sender.js +9 -0
  85. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  86. package/build/src/host/whatsapp_senders/http.js +33 -0
  87. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  88. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  89. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  90. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  91. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  92. package/build/src/mcp/mcp_oauth.js +154 -0
  93. package/build/src/provider/build_provider.js +15 -1
  94. package/build/src/provider/oidc_service.d.ts +8 -0
  95. package/build/src/provider/oidc_service.js +16 -2
  96. package/build/src/schema/ensure.js +92 -0
  97. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  98. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  99. package/package.json +2 -1
  100. package/stubs/ui/react/pages/consent.tsx +17 -1
  101. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -1,7 +1,10 @@
1
1
  import '../augmentations.js';
2
+ import { displayNameFor } from '../../agents/agent_identity.js';
3
+ import { personalAgentsFor } from '../../agents/runtime.js';
2
4
  import { accountPath } from '../account_paths.js';
3
5
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
4
6
  import { AdminSessionsService } from '../admin_sessions_service.js';
7
+ import { clientDisplayNames } from '../client_names.js';
5
8
  /**
6
9
  * Self-service de consentimento ("apps com acesso") no console de conta. Lista os
7
10
  * Grants da própria conta agrupados por client (resolvendo o nome do client da
@@ -19,13 +22,26 @@ export default class AccountAppsController {
19
22
  const sessions = new AdminSessionsService(service);
20
23
  const supported = sessions.canList;
21
24
  const grantList = supported ? await sessions.listGrants(accountId) : [];
22
- // Resolve o nome amigável do client: clientId é o fallback (config estática não
23
- // carrega um display name).
24
- const nameOf = (clientId) => clientId ?? '';
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) : '';
25
28
  const revoked = ctx.session.flashMessages.get('appRevoked');
29
+ // Personal agents com delegação ativa (só quando a feature está ligada).
30
+ const agentsRuntime = await personalAgentsFor(ctx);
31
+ const agents = agentsRuntime?.delegation
32
+ ? await Promise.all((await agentsRuntime.delegation.listGrants(accountId)).map(async (g) => ({
33
+ id: g.id,
34
+ name: displayNameFor((await agentsRuntime.config.resolveAgent(g.clientId)) ?? {
35
+ issuer: g.clientId,
36
+ jwksUri: '',
37
+ }),
38
+ scopes: g.scopes,
39
+ })))
40
+ : null;
26
41
  return render(ctx, 'account/apps', {
27
42
  csrfToken: ctx.request.csrfToken,
28
43
  supported,
44
+ agents,
29
45
  revoked: revoked ?? null,
30
46
  apps: grantList
31
47
  .filter((g) => !!g.clientId)
@@ -59,4 +75,24 @@ export default class AccountAppsController {
59
75
  ctx.session.flash('appRevoked', cfg.messages['account.apps.revoked'] ?? 'account.apps.revoked');
60
76
  return ctx.response.redirect(accountPath('apps'));
61
77
  }
78
+ /** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
79
+ async revokeAgent(ctx) {
80
+ const runtime = await personalAgentsFor(ctx);
81
+ if (!runtime?.delegation)
82
+ return ctx.response.notFound();
83
+ const service = await ctx.containerResolver.make('authkit.server');
84
+ const cfg = service.config;
85
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
86
+ const grantId = ctx.request.param('grantId');
87
+ if (await runtime.delegation.revokeGrant(accountId, grantId)) {
88
+ await cfg.audit?.record({
89
+ type: 'agent.grant_revoked',
90
+ accountId,
91
+ ip: ctx.request.ip?.() ?? null,
92
+ metadata: { grantId },
93
+ });
94
+ }
95
+ ctx.session.flash('appRevoked', cfg.messages['account.apps.revoked'] ?? 'account.apps.revoked');
96
+ return ctx.response.redirect(accountPath('apps'));
97
+ }
62
98
  }
@@ -11,6 +11,7 @@ import { revokeOrgAccess } from '../org_access_revocation.js';
11
11
  // ponto de verdade único, para as duas superfícies nunca divergirem.
12
12
  import { effectiveOrgPolicy, orgPolicyDefaults } from '../org_policy.js';
13
13
  import { authkitOrigin } from '../origin.js';
14
+ import { redirectExact } from '../redirect_exact.js';
14
15
  import { resolveRuntimeSettings } from '../runtime_settings.js';
15
16
  import { isRoleInCatalog, resolveEffectiveOrganizationsPolicy } from '../runtime_toggles.js';
16
17
  /**
@@ -257,7 +258,7 @@ export default class AccountOrgsController {
257
258
  const loginUrl = getAccountLoginUrl();
258
259
  const returnTo = encodeURIComponent(`${accountPath('orgs')}/invitations/${params.token}/accept`);
259
260
  const sep = loginUrl.includes('?') ? '&' : '?';
260
- return response.redirect(`${loginUrl}${sep}returnTo=${returnTo}`);
261
+ return redirectExact(response, `${loginUrl}${sep}returnTo=${returnTo}`);
261
262
  }
262
263
  const tokenHash = createHash('sha256').update(params.token).digest('hex');
263
264
  const invitation = await store.findInvitationByTokenHash(tokenHash);
@@ -158,15 +158,19 @@ export default class AccountSecurityController {
158
158
  return ctx.response.redirect(getAccountLoginUrl());
159
159
  }
160
160
  const { currentPassword, confirmEmail } = await ctx.request.validateUsing(deleteAccountValidator);
161
- // Confirmação: senha atual correta OU e-mail digitado batendo com o da conta
162
- // (case-insensitive). Sem nenhuma das duas → recusa (não deleta).
161
+ // Confirmação: senha atual correta OU e-mail digitado batendo com o da conta.
162
+ // Contas sem e-mail podem confirmar o telefone já verificado pelo host.
163
163
  let confirmed = false;
164
164
  if (currentPassword) {
165
165
  confirmed = !!(await store.verifyCredentials(account.email, currentPassword));
166
166
  }
167
167
  if (!confirmed && confirmEmail) {
168
- confirmed =
169
- normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email);
168
+ confirmed = account.email
169
+ ? normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email)
170
+ : !!account.phone &&
171
+ /^[+\d\s().-]+$/.test(confirmEmail) &&
172
+ /\d/.test(confirmEmail) &&
173
+ confirmEmail.replace(/\D/g, '') === account.phone.replace(/\D/g, '');
170
174
  }
171
175
  if (!confirmed) {
172
176
  ctx.session.flash('deleteError', translate(cfg.messages, 'account.delete.invalid_confirmation'));
@@ -8,6 +8,7 @@ import { translate } from '../i18n.js';
8
8
  import { endBridgedIdpSession } from '../idp_session_bridge.js';
9
9
  import { attemptPasswordLogin } from '../login_attempt.js';
10
10
  import { notifyLoginSuccess } from '../login_notify.js';
11
+ import { redirectExact } from '../redirect_exact.js';
11
12
  import { resolveRuntimeSettings } from '../runtime_settings.js';
12
13
  import { markSudo } from '../sudo_mode.js';
13
14
  /**
@@ -110,7 +111,7 @@ export default class AccountSessionController {
110
111
  await syncAdonisAuthLogin(ctx, cfg, acc);
111
112
  await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email: acc.email, ip });
112
113
  // Redireciona pro destino original (validado), ou cai no accountHome configurado.
113
- return ctx.response.redirect(returnTo ?? accountHome(cfg));
114
+ return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
114
115
  }
115
116
  async logout(ctx) {
116
117
  const service = await ctx.containerResolver.make('authkit.server');
@@ -0,0 +1,17 @@
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ /**
4
+ * Tela de consentimento da delegação (PACT §5.3) — o `verification_uri` do
5
+ * device flow. Montada atrás do `accountGuard`: sem sessão o usuário passa pelo
6
+ * login DESTE app e volta com o `user_code` no `return_to`. O agente nunca vê
7
+ * esse login.
8
+ *
9
+ * Durante uma impersonation a tela recusa: um admin personificando alguém não
10
+ * pode entregar a conta dessa pessoa a um agente.
11
+ */
12
+ export default class AgentConsentController {
13
+ /** GET {prefix}/consent?user_code=XXXX-XXXX */
14
+ show(ctx: HttpContext): Promise<any>;
15
+ /** POST {prefix}/consent — `decision=allow|deny`, `scope[]` marcados. */
16
+ decide(ctx: HttpContext): Promise<any>;
17
+ }
@@ -0,0 +1,138 @@
1
+ import '../augmentations.js';
2
+ import { displayNameFor } from '../../agents/agent_identity.js';
3
+ import { personalAgentsFor } from '../../agents/runtime.js';
4
+ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
5
+ import { impersonationState } from '../impersonation_session.js';
6
+ async function agentName(runtime, issuer) {
7
+ const registration = await runtime.config.resolveAgent(issuer);
8
+ return displayNameFor(registration ?? { issuer, jwksUri: '' });
9
+ }
10
+ /** Host do issuer para exibição — um resolver do host pode devolver issuer que não é URL. */
11
+ function originOf(issuer) {
12
+ try {
13
+ return new URL(issuer).host;
14
+ }
15
+ catch {
16
+ return issuer;
17
+ }
18
+ }
19
+ /**
20
+ * Anti-clickjacking: o atacante pega um código na própria sessão de agente e
21
+ * emoldura o link já preenchido — um clique em "Permitir" entregaria a conta.
22
+ * A tela nunca pode ser renderizada num frame, com ou sem o shield do host.
23
+ */
24
+ function denyFraming(ctx) {
25
+ ctx.response.header('X-Frame-Options', 'DENY');
26
+ ctx.response.header('Content-Security-Policy', "frame-ancestors 'none'");
27
+ }
28
+ /**
29
+ * Tela de consentimento da delegação (PACT §5.3) — o `verification_uri` do
30
+ * device flow. Montada atrás do `accountGuard`: sem sessão o usuário passa pelo
31
+ * login DESTE app e volta com o `user_code` no `return_to`. O agente nunca vê
32
+ * esse login.
33
+ *
34
+ * Durante uma impersonation a tela recusa: um admin personificando alguém não
35
+ * pode entregar a conta dessa pessoa a um agente.
36
+ */
37
+ export default class AgentConsentController {
38
+ /** GET {prefix}/consent?user_code=XXXX-XXXX */
39
+ async show(ctx) {
40
+ const runtime = await personalAgentsFor(ctx);
41
+ if (!runtime?.delegation)
42
+ return ctx.response.notFound();
43
+ denyFraming(ctx);
44
+ const service = await ctx.containerResolver.make('authkit.server');
45
+ const render = service.config.render;
46
+ const base = {
47
+ csrfToken: ctx.request.csrfToken,
48
+ action: runtime.urls.consent,
49
+ request: null,
50
+ account: null,
51
+ error: null,
52
+ };
53
+ if (impersonationState(ctx).active) {
54
+ return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.impersonating' });
55
+ }
56
+ const input = ctx.request.input('user_code');
57
+ if (!input)
58
+ return render(ctx, 'agents/consent', base);
59
+ const pending = await runtime.delegation.pendingRequest(input);
60
+ if (!pending) {
61
+ return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.invalid_code' });
62
+ }
63
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
64
+ const account = await service.config.accountStore.findById(accountId);
65
+ return render(ctx, 'agents/consent', {
66
+ ...base,
67
+ request: {
68
+ userCode: pending.userCode,
69
+ agentName: await agentName(runtime, pending.clientId),
70
+ agentOrigin: originOf(pending.clientId),
71
+ scopes: pending.scopes,
72
+ },
73
+ account: { email: account?.email ?? null },
74
+ });
75
+ }
76
+ /** POST {prefix}/consent — `decision=allow|deny`, `scope[]` marcados. */
77
+ async decide(ctx) {
78
+ const runtime = await personalAgentsFor(ctx);
79
+ if (!runtime?.delegation)
80
+ return ctx.response.notFound();
81
+ denyFraming(ctx);
82
+ const service = await ctx.containerResolver.make('authkit.server');
83
+ const cfg = service.config;
84
+ const render = cfg.render;
85
+ if (impersonationState(ctx).active) {
86
+ return render(ctx, 'agents/consent', {
87
+ csrfToken: ctx.request.csrfToken,
88
+ action: runtime.urls.consent,
89
+ request: null,
90
+ account: null,
91
+ error: 'agents.consent.impersonating',
92
+ });
93
+ }
94
+ const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
95
+ const rawCode = ctx.request.input('user_code');
96
+ const userCode = typeof rawCode === 'string' ? rawCode : '';
97
+ const pending = await runtime.delegation.pendingRequest(userCode);
98
+ if (!pending) {
99
+ return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
100
+ }
101
+ const name = await agentName(runtime, pending.clientId);
102
+ const raw = ctx.request.input('scope');
103
+ const chosen = (Array.isArray(raw) ? raw : raw ? [raw] : []).map(String);
104
+ const approved = ctx.request.input('decision') === 'allow'
105
+ ? await runtime.delegation.approve({ userCode, accountId, scopes: chosen })
106
+ : null;
107
+ if (!approved) {
108
+ // Negou ou desmarcou tudo. Se nem negar deu, o pedido mudou de estado no
109
+ // meio do caminho (aprovado em outra aba, expirou).
110
+ if (!(await runtime.delegation.deny({ userCode, accountId }))) {
111
+ return render(ctx, 'agents/done', { status: 'expired', agentName: name, scopes: [] });
112
+ }
113
+ await cfg.audit?.record({
114
+ type: 'agent.delegation_denied',
115
+ accountId,
116
+ clientId: pending.clientId,
117
+ ip: ctx.request.ip?.() ?? null,
118
+ metadata: { requested: pending.scopes.map((s) => s.id) },
119
+ });
120
+ return render(ctx, 'agents/done', { status: 'denied', agentName: name, scopes: [] });
121
+ }
122
+ await cfg.audit?.record({
123
+ type: 'agent.delegation_approved',
124
+ accountId,
125
+ clientId: pending.clientId,
126
+ ip: ctx.request.ip?.() ?? null,
127
+ metadata: { grantId: approved.grantId, scopes: approved.scopes },
128
+ });
129
+ return render(ctx, 'agents/done', {
130
+ status: 'approved',
131
+ agentName: name,
132
+ scopes: approved.scopes.map((id) => ({
133
+ id,
134
+ description: runtime.delegation.scopes[id] ?? id,
135
+ })),
136
+ });
137
+ }
138
+ }
@@ -0,0 +1,23 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * Authorization server de delegação para personal agents (PACT §5.3): metadata
4
+ * RFC 8414, JWKS e os endpoints RFC 8628 que o AGENTE chama. O cliente OAuth é
5
+ * o agente, autenticado pelo JWT dele em `Authorization: Bearer` em toda
6
+ * chamada — `client_id` tem de ser o `iss` desse JWT.
7
+ */
8
+ export default class AgentOAuthController {
9
+ #private;
10
+ metadata(ctx: HttpContext): Promise<void | {
11
+ issuer: string;
12
+ device_authorization_endpoint: string;
13
+ token_endpoint: string;
14
+ jwks_uri: string;
15
+ scopes_supported: string[];
16
+ grant_types_supported: string[];
17
+ response_types_supported: never[];
18
+ }>;
19
+ /** Chaves que assinam tokens de delegação e recibos — as mesmas do IdP. */
20
+ jwks(ctx: HttpContext): Promise<any>;
21
+ deviceAuthorization(ctx: HttpContext): Promise<void>;
22
+ token(ctx: HttpContext): Promise<void>;
23
+ }
@@ -0,0 +1,110 @@
1
+ import { AgentOAuthError, DEVICE_CODE_GRANT_TYPE, REFRESH_TOKEN_GRANT_TYPE, } from '../../agents/delegation_service.js';
2
+ import { personalAgentUnauthorized } from '../../agents/middleware.js';
3
+ import { personalAgentsFor } from '../../agents/runtime.js';
4
+ function noStore(ctx) {
5
+ ctx.response.header('Cache-Control', 'no-store');
6
+ ctx.response.header('Pragma', 'no-cache');
7
+ }
8
+ function oauthError(ctx, error) {
9
+ noStore(ctx);
10
+ return ctx.response
11
+ .status(error.status)
12
+ .json({ error: error.code, error_description: error.description });
13
+ }
14
+ /**
15
+ * Authorization server de delegação para personal agents (PACT §5.3): metadata
16
+ * RFC 8414, JWKS e os endpoints RFC 8628 que o AGENTE chama. O cliente OAuth é
17
+ * o agente, autenticado pelo JWT dele em `Authorization: Bearer` em toda
18
+ * chamada — `client_id` tem de ser o `iss` desse JWT.
19
+ */
20
+ export default class AgentOAuthController {
21
+ async metadata(ctx) {
22
+ const runtime = await personalAgentsFor(ctx);
23
+ if (!runtime?.delegation)
24
+ return ctx.response.notFound();
25
+ ctx.response.header('Access-Control-Allow-Origin', '*');
26
+ return {
27
+ issuer: runtime.urls.issuer,
28
+ device_authorization_endpoint: runtime.urls.deviceAuthorization,
29
+ token_endpoint: runtime.urls.token,
30
+ jwks_uri: runtime.urls.jwks,
31
+ scopes_supported: Object.keys(runtime.delegation.scopes),
32
+ grant_types_supported: [DEVICE_CODE_GRANT_TYPE, REFRESH_TOKEN_GRANT_TYPE],
33
+ response_types_supported: [],
34
+ // Sem `token_endpoint_auth_methods_supported`: o agente se autentica com o
35
+ // JWT dele em `Authorization: Bearer` (PACT §5.3), método sem nome
36
+ // registrado — anunciar `private_key_jwt` levaria um cliente genérico a
37
+ // mandar `client_assertion` e tomar 401.
38
+ };
39
+ }
40
+ /** Chaves que assinam tokens de delegação e recibos — as mesmas do IdP. */
41
+ async jwks(ctx) {
42
+ const runtime = await personalAgentsFor(ctx);
43
+ if (!runtime?.delegation)
44
+ return ctx.response.notFound();
45
+ const service = await ctx.containerResolver.make('authkit.server');
46
+ ctx.response.header('Access-Control-Allow-Origin', '*');
47
+ ctx.response.header('Cache-Control', 'public, max-age=300');
48
+ return service.publicJwks;
49
+ }
50
+ async deviceAuthorization(ctx) {
51
+ const auth = await this.#authenticate(ctx);
52
+ if (!auth)
53
+ return;
54
+ try {
55
+ const body = await auth.runtime.delegation.requestDevice(auth.agent, ctx.request.input('scope'));
56
+ noStore(ctx);
57
+ return ctx.response.json(body);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof AgentOAuthError)
61
+ return oauthError(ctx, error);
62
+ throw error;
63
+ }
64
+ }
65
+ async token(ctx) {
66
+ const auth = await this.#authenticate(ctx);
67
+ if (!auth)
68
+ return;
69
+ const delegation = auth.runtime.delegation;
70
+ try {
71
+ const grantType = ctx.request.input('grant_type');
72
+ const body = grantType === DEVICE_CODE_GRANT_TYPE
73
+ ? await delegation.exchangeDeviceCode(auth.agent, ctx.request.input('device_code'))
74
+ : grantType === REFRESH_TOKEN_GRANT_TYPE
75
+ ? await delegation.refresh(auth.agent, ctx.request.input('refresh_token'))
76
+ : null;
77
+ if (!body) {
78
+ throw new AgentOAuthError('unsupported_grant_type', 'Unsupported grant_type');
79
+ }
80
+ noStore(ctx);
81
+ return ctx.response.json(body);
82
+ }
83
+ catch (error) {
84
+ if (error instanceof AgentOAuthError)
85
+ return oauthError(ctx, error);
86
+ throw error;
87
+ }
88
+ }
89
+ /**
90
+ * JWT do agente + `client_id` coerente. Já respondeu (401/404) quando
91
+ * devolve `null`.
92
+ */
93
+ async #authenticate(ctx) {
94
+ const runtime = await personalAgentsFor(ctx);
95
+ if (!runtime?.delegation) {
96
+ ctx.response.notFound();
97
+ return null;
98
+ }
99
+ const agent = await runtime.verifier.verify(ctx.request.header('authorization'));
100
+ if (!agent) {
101
+ personalAgentUnauthorized(ctx, runtime.config.protocol);
102
+ return null;
103
+ }
104
+ if (ctx.request.input('client_id') !== agent.issuer) {
105
+ oauthError(ctx, new AgentOAuthError('invalid_client', 'client_id must equal the personal-agent issuer', 401));
106
+ return null;
107
+ }
108
+ return { runtime, agent };
109
+ }
110
+ }
@@ -1,7 +1,14 @@
1
1
  import '../augmentations.js';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
+ import { type CustomLoginIdentity } from '../custom_login.js';
3
4
  export default class AuthInteractionController {
4
5
  #private;
6
+ /** Shared completion for trusted host authentication; primary proof is the host's responsibility. */
7
+ completeCustomLogin(ctx: HttpContext, input: CustomLoginIdentity & {
8
+ method: string;
9
+ factorId?: string;
10
+ passwordless?: boolean;
11
+ }): Promise<unknown>;
5
12
  show(ctx: HttpContext): Promise<any>;
6
13
  /**
7
14
  * POST /auth/interaction/:uid/identifier
@@ -29,7 +36,12 @@ export default class AuthInteractionController {
29
36
  * 2º fator: lê o accountId pendente da sessão e aceita um código TOTP (`code`)
30
37
  * OU um recovery code (`recoveryCode`). Em caso de sucesso finaliza a interaction.
31
38
  */
32
- mfaVerify(ctx: HttpContext): Promise<any>;
39
+ private renderMfaChallenge;
40
+ private configuredMfaGate;
41
+ customMfaBegin(ctx: HttpContext): Promise<unknown>;
42
+ customMfaVerify(ctx: HttpContext): Promise<unknown>;
43
+ private finishVerifiedMfa;
44
+ mfaVerify(ctx: HttpContext): Promise<unknown>;
33
45
  /**
34
46
  * true se o authorize request exige MFA via acr_values (contém o mfaAcr da
35
47
  * config de step-up). `acr_values` é a string separada por espaços padrão OIDC.