@adonis-agora/authkit-server 0.74.0 → 0.76.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 (79) hide show
  1. package/README.md +3 -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/partials/styles.edge +1 -1
  7. package/build/index.d.ts +9 -0
  8. package/build/index.js +4 -0
  9. package/build/providers/authkit_server_provider.js +75 -50
  10. package/build/src/agents/agent_identity.d.ts +33 -0
  11. package/build/src/agents/agent_identity.js +156 -0
  12. package/build/src/agents/config.d.ts +99 -0
  13. package/build/src/agents/config.js +101 -0
  14. package/build/src/agents/delegation_service.d.ts +154 -0
  15. package/build/src/agents/delegation_service.js +394 -0
  16. package/build/src/agents/delegation_store.d.ts +93 -0
  17. package/build/src/agents/delegation_store.js +222 -0
  18. package/build/src/agents/middleware.d.ts +73 -0
  19. package/build/src/agents/middleware.js +113 -0
  20. package/build/src/agents/protocol.d.ts +62 -0
  21. package/build/src/agents/protocol.js +51 -0
  22. package/build/src/agents/runtime.d.ts +36 -0
  23. package/build/src/agents/runtime.js +65 -0
  24. package/build/src/agents/signer.d.ts +34 -0
  25. package/build/src/agents/signer.js +70 -0
  26. package/build/src/audit/audit_sink.d.ts +1 -1
  27. package/build/src/audit/audit_sink.js +5 -0
  28. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  29. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  30. package/build/src/define_config.d.ts +34 -3
  31. package/build/src/define_config.js +35 -1
  32. package/build/src/host/access_token_verifier.d.ts +7 -0
  33. package/build/src/host/access_token_verifier.js +8 -0
  34. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  35. package/build/src/host/account_api/account_api_controller.js +40 -0
  36. package/build/src/host/admin_api/dto.d.ts +1 -1
  37. package/build/src/host/admin_sessions_service.d.ts +2 -0
  38. package/build/src/host/admin_sessions_service.js +18 -0
  39. package/build/src/host/auth_host_config.d.ts +4 -0
  40. package/build/src/host/bearer_account.d.ts +17 -0
  41. package/build/src/host/bearer_account.js +10 -0
  42. package/build/src/host/client_names.d.ts +11 -0
  43. package/build/src/host/client_names.js +12 -0
  44. package/build/src/host/console_session.js +3 -5
  45. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  46. package/build/src/host/controllers/account_apps_controller.js +39 -3
  47. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  48. package/build/src/host/controllers/account_session_controller.js +2 -1
  49. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  50. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  51. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  52. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  53. package/build/src/host/controllers/interaction_controller.js +28 -0
  54. package/build/src/host/csrf.d.ts +8 -18
  55. package/build/src/host/csrf.js +28 -2
  56. package/build/src/host/i18n.d.ts +54 -0
  57. package/build/src/host/i18n.js +54 -0
  58. package/build/src/host/impersonation_session.d.ts +5 -0
  59. package/build/src/host/impersonation_session.js +37 -4
  60. package/build/src/host/oidc_bearer_guard.js +10 -1
  61. package/build/src/host/redirect_exact.d.ts +12 -0
  62. package/build/src/host/redirect_exact.js +17 -0
  63. package/build/src/host/register_auth_host.js +48 -7
  64. package/build/src/host/request_url.d.ts +10 -0
  65. package/build/src/host/request_url.js +17 -0
  66. package/build/src/host/sudo/methods/magic_link.js +2 -1
  67. package/build/src/host/sudo/runtime.js +3 -2
  68. package/build/src/host/sudo_mode.js +4 -4
  69. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  70. package/build/src/mcp/mcp_oauth.js +154 -0
  71. package/build/src/provider/build_provider.js +20 -1
  72. package/build/src/provider/oidc_service.d.ts +8 -0
  73. package/build/src/provider/oidc_service.js +11 -0
  74. package/build/src/provider/token_exchange.d.ts +26 -0
  75. package/build/src/provider/token_exchange.js +44 -3
  76. package/build/src/schema/ensure.js +92 -0
  77. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  78. package/package.json +1 -1
  79. package/stubs/ui/react/pages/consent.tsx +17 -1
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Nome de exibição de cada client: o `client_name` que ele registrou (um cliente MCP registra o
3
+ * seu, ex.: "Claude Code"), senão o próprio `client_id`. Client que sumiu do adapter → o id.
4
+ */
5
+ export async function clientDisplayNames(service, clientIds) {
6
+ const names = new Map();
7
+ await Promise.all([...new Set(clientIds)].map(async (clientId) => {
8
+ const client = await service.provider.Client.find(clientId).catch(() => null);
9
+ names.set(clientId, client?.clientName || clientId);
10
+ }));
11
+ return names;
12
+ }
@@ -65,11 +65,9 @@ export function getAccountId(ctx) {
65
65
  * if (!id || !(await authz.hasRole(id, 'admin'))) throw new Error('forbidden')
66
66
  */
67
67
  export function realAccountId(ctx) {
68
- // Sem sessão não há impersonation: mesma tolerância do `getAccountId` (que usa
69
- // `ctx.session?.`), pois `impersonationState` assume uma sessão presente. Resta
70
- // a identidade bearer (se o `oidcBearerGuard` autenticou a request).
71
- if (!ctx.session)
72
- return bearerAccountId(ctx);
68
+ // `impersonationState` cobre as duas fontes: a sessão do console e o access
69
+ // token bearer trocado (`act`, ex.: app nativo) — nos dois, o humano real é o
70
+ // impersonator. Sem impersonation, a conta da sessão ou do bearer.
73
71
  return impersonationState(ctx).impersonatorId ?? getAccountId(ctx);
74
72
  }
75
73
  /**
@@ -12,4 +12,6 @@ export default class AccountAppsController {
12
12
  index(ctx: HttpContext): Promise<any>;
13
13
  /** POST /account/apps/:clientId/revoke — revoga o acesso de um client. */
14
14
  revoke(ctx: HttpContext): Promise<void>;
15
+ /** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
16
+ revokeAgent(ctx: HttpContext): Promise<void>;
15
17
  }
@@ -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);
@@ -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
+ }
@@ -180,6 +180,7 @@ export default class AuthInteractionController {
180
180
  params: details.params,
181
181
  csrfToken: ctx.request.csrfToken,
182
182
  brand,
183
+ ...(await consentSubject(service, details, cfg.messages)),
183
184
  });
184
185
  }
185
186
  const email = ctx.session.get(SESSION_KEY);
@@ -1564,3 +1565,30 @@ export default class AuthInteractionController {
1564
1565
  }
1565
1566
  }
1566
1567
  }
1568
+ /**
1569
+ * Quem pede o acesso e o quê, para a tela de consent: o nome que o client registrou (um cliente
1570
+ * MCP registra o seu, ex.: "Claude Code") e os escopos pedidos, com o rótulo de cada um. O nome
1571
+ * vem de fora (registro dinâmico aberto) — `clientNameHtml` sai escapado para o `{{{ }}}`.
1572
+ */
1573
+ async function consentSubject(service, details, messages) {
1574
+ const clientId = String(details.params?.client_id ?? '');
1575
+ const client = clientId ? await service.provider.Client.find(clientId).catch(() => null) : null;
1576
+ const clientName = client?.clientName || clientId;
1577
+ const requested = String(details.params?.scope ?? '')
1578
+ .split(' ')
1579
+ .filter((scope) => scope !== '' && scope !== 'openid');
1580
+ const scopes = requested.map((scope) => {
1581
+ const key = `consent.scope.${scope}`;
1582
+ const label = translate(messages, key);
1583
+ return { id: scope, label: label === key ? scope : label };
1584
+ });
1585
+ return { clientName, clientNameHtml: escapeHtml(clientName), scopes };
1586
+ }
1587
+ function escapeHtml(value) {
1588
+ return value
1589
+ .replace(/&/g, '&amp;')
1590
+ .replace(/</g, '&lt;')
1591
+ .replace(/>/g, '&gt;')
1592
+ .replace(/"/g, '&quot;')
1593
+ .replace(/'/g, '&#39;');
1594
+ }
@@ -1,29 +1,19 @@
1
- /**
2
- * Helper de exceções de CSRF para as rotas machine-to-machine do AuthKit.
3
- *
4
- * Os endpoints do IdP (token/introspection/PAT) e o back-channel logout são
5
- * server-to-server (sem browser/token XSRF). Em vez de cada app reescrever a
6
- * checagem de URL no `config/shield.ts`, use este helper:
7
- *
8
- * ```ts
9
- * // config/shield.ts
10
- * import { authkitCsrfExceptions } from '@adonis-agora/authkit-server'
11
- * csrf: {
12
- * enabled: true,
13
- * exceptRoutes: (ctx) => authkitCsrfExceptions(ctx.request.url(), { mountPath: '/oidc' })
14
- * || ctx.request.url().includes('/api'),
15
- * }
16
- * ```
17
- */
18
1
  export interface AuthkitCsrfOptions {
19
2
  /** mountPath do IdP (mesmo de defineConfig/registerAuthHost). Default: `/oidc`. */
20
3
  mountPath?: string;
21
4
  /** Inclui a rota de back-channel logout do CLIENT (default: `/auth/backchannel-logout`). */
22
5
  backchannelLogoutPath?: string | false;
6
+ /**
7
+ * Prefixo de `personalAgents`: isenta os endpoints OAuth que os agentes chamam
8
+ * (`{prefix}/oauth/*`). Default: o prefixo do config quando `personalAgents`
9
+ * está ligado; nada quando não está. `false` desliga. A tela de
10
+ * consentimento (`{prefix}/consent`) continua protegida.
11
+ */
12
+ personalAgentsPrefix?: string | false;
23
13
  }
24
14
  /**
25
15
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
26
16
  * (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
27
- * rota de back-channel logout do client.
17
+ * rota de back-channel logout do client e os endpoints OAuth de personal agents.
28
18
  */
29
19
  export declare function authkitCsrfExceptions(url: string, options?: AuthkitCsrfOptions): boolean;
@@ -1,14 +1,40 @@
1
+ /**
2
+ * Helper de exceções de CSRF para as rotas machine-to-machine do AuthKit.
3
+ *
4
+ * Os endpoints do IdP (token/introspection/PAT) e o back-channel logout são
5
+ * server-to-server (sem browser/token XSRF). Em vez de cada app reescrever a
6
+ * checagem de URL no `config/shield.ts`, use este helper:
7
+ *
8
+ * ```ts
9
+ * // config/shield.ts
10
+ * import { authkitCsrfExceptions } from '@adonis-agora/authkit-server'
11
+ * csrf: {
12
+ * enabled: true,
13
+ * exceptRoutes: (ctx) => authkitCsrfExceptions(ctx.request.url(), { mountPath: '/oidc' })
14
+ * || ctx.request.url().includes('/api'),
15
+ * }
16
+ * ```
17
+ */
18
+ import { normalizePersonalAgentsPrefix } from '../agents/config.js';
19
+ import { getAuthHostConfig } from './auth_host_config.js';
1
20
  /**
2
21
  * Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
3
22
  * (machine-to-machine). Cobre o mountPath do IdP, a introspecção de PAT e a
4
- * rota de back-channel logout do client.
23
+ * rota de back-channel logout do client e os endpoints OAuth de personal agents.
5
24
  */
6
25
  export function authkitCsrfExceptions(url, options = {}) {
7
26
  const mountPath = options.mountPath ?? '/oidc';
8
27
  const backchannel = options.backchannelLogoutPath === false
9
28
  ? null
10
29
  : (options.backchannelLogoutPath ?? '/auth/backchannel-logout');
30
+ const agentsPrefix = options.personalAgentsPrefix === false
31
+ ? undefined
32
+ : options.personalAgentsPrefix !== undefined
33
+ ? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
34
+ : getAuthHostConfig()?.personalAgents?.prefix;
35
+ const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
11
36
  return (url.includes(mountPath) ||
12
37
  url.includes('/authkit/pat') ||
13
- (backchannel !== null && url === backchannel));
38
+ (backchannel !== null && url === backchannel) ||
39
+ (agentsOAuth !== null && url.startsWith(agentsOAuth)));
14
40
  }
@@ -97,6 +97,11 @@ export declare const DEFAULT_MESSAGES: {
97
97
  'consent.title': string;
98
98
  'consent.body': string;
99
99
  'consent.submit': string;
100
+ 'consent.scopes_title': string;
101
+ 'consent.scope.profile': string;
102
+ 'consent.scope.email': string;
103
+ 'consent.scope.offline_access': string;
104
+ 'consent.scope.roles': string;
100
105
  'account.login.page_title': string;
101
106
  'account.login.title': string;
102
107
  'account.login.intro': string;
@@ -175,6 +180,28 @@ export declare const DEFAULT_MESSAGES: {
175
180
  'account.apps.revoke': string;
176
181
  'account.apps.revoke_confirm': string;
177
182
  'account.apps.revoked': string;
183
+ 'account.apps.agents_title': string;
184
+ 'account.apps.agents_intro': string;
185
+ 'account.apps.agents_empty': string;
186
+ 'agents.consent.page_title': string;
187
+ 'agents.consent.title': string;
188
+ 'agents.consent.body': string;
189
+ 'agents.consent.signed_in_as': string;
190
+ 'agents.consent.scopes_label': string;
191
+ 'agents.consent.code': string;
192
+ 'agents.consent.allow': string;
193
+ 'agents.consent.deny': string;
194
+ 'agents.consent.enter_code_title': string;
195
+ 'agents.consent.enter_code_body': string;
196
+ 'agents.consent.continue': string;
197
+ 'agents.consent.invalid_code': string;
198
+ 'agents.consent.impersonating': string;
199
+ 'agents.done.approved_title': string;
200
+ 'agents.done.denied_title': string;
201
+ 'agents.done.return': string;
202
+ 'agents.done.revoke_hint': string;
203
+ 'agents.done.expired_title': string;
204
+ 'agents.done.expired_body': string;
178
205
  'account.apps.not_supported': string;
179
206
  'account.orgs.page_title': string;
180
207
  'account.orgs.title': string;
@@ -820,6 +847,11 @@ export declare const PT_BR_MESSAGES: {
820
847
  'consent.title': string;
821
848
  'consent.body': string;
822
849
  'consent.submit': string;
850
+ 'consent.scopes_title': string;
851
+ 'consent.scope.profile': string;
852
+ 'consent.scope.email': string;
853
+ 'consent.scope.offline_access': string;
854
+ 'consent.scope.roles': string;
823
855
  'account.login.page_title': string;
824
856
  'account.login.title': string;
825
857
  'account.login.intro': string;
@@ -898,6 +930,28 @@ export declare const PT_BR_MESSAGES: {
898
930
  'account.apps.revoke': string;
899
931
  'account.apps.revoke_confirm': string;
900
932
  'account.apps.revoked': string;
933
+ 'account.apps.agents_title': string;
934
+ 'account.apps.agents_intro': string;
935
+ 'account.apps.agents_empty': string;
936
+ 'agents.consent.page_title': string;
937
+ 'agents.consent.title': string;
938
+ 'agents.consent.body': string;
939
+ 'agents.consent.signed_in_as': string;
940
+ 'agents.consent.scopes_label': string;
941
+ 'agents.consent.code': string;
942
+ 'agents.consent.allow': string;
943
+ 'agents.consent.deny': string;
944
+ 'agents.consent.enter_code_title': string;
945
+ 'agents.consent.enter_code_body': string;
946
+ 'agents.consent.continue': string;
947
+ 'agents.consent.invalid_code': string;
948
+ 'agents.consent.impersonating': string;
949
+ 'agents.done.approved_title': string;
950
+ 'agents.done.denied_title': string;
951
+ 'agents.done.return': string;
952
+ 'agents.done.revoke_hint': string;
953
+ 'agents.done.expired_title': string;
954
+ 'agents.done.expired_body': string;
901
955
  'account.apps.not_supported': string;
902
956
  'account.orgs.page_title': string;
903
957
  'account.orgs.title': string;