@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.
- package/README.md +3 -0
- package/build/host/views/account/apps.edge +30 -0
- package/build/host/views/agents/consent.edge +60 -0
- package/build/host/views/agents/done.edge +24 -0
- package/build/host/views/consent.edge +9 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +9 -0
- package/build/index.js +4 -0
- package/build/providers/authkit_server_provider.js +75 -50
- package/build/src/agents/agent_identity.d.ts +33 -0
- package/build/src/agents/agent_identity.js +156 -0
- package/build/src/agents/config.d.ts +99 -0
- package/build/src/agents/config.js +101 -0
- package/build/src/agents/delegation_service.d.ts +154 -0
- package/build/src/agents/delegation_service.js +394 -0
- package/build/src/agents/delegation_store.d.ts +93 -0
- package/build/src/agents/delegation_store.js +222 -0
- package/build/src/agents/middleware.d.ts +73 -0
- package/build/src/agents/middleware.js +113 -0
- package/build/src/agents/protocol.d.ts +62 -0
- package/build/src/agents/protocol.js +51 -0
- package/build/src/agents/runtime.d.ts +36 -0
- package/build/src/agents/runtime.js +65 -0
- package/build/src/agents/signer.d.ts +34 -0
- package/build/src/agents/signer.js +70 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
- package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
- package/build/src/define_config.d.ts +34 -3
- package/build/src/define_config.js +35 -1
- package/build/src/host/access_token_verifier.d.ts +7 -0
- package/build/src/host/access_token_verifier.js +8 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -0
- package/build/src/host/account_api/account_api_controller.js +40 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_sessions_service.d.ts +2 -0
- package/build/src/host/admin_sessions_service.js +18 -0
- package/build/src/host/auth_host_config.d.ts +4 -0
- package/build/src/host/bearer_account.d.ts +17 -0
- package/build/src/host/bearer_account.js +10 -0
- package/build/src/host/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- package/build/src/host/console_session.js +3 -5
- package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
- package/build/src/host/controllers/account_apps_controller.js +39 -3
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
- package/build/src/host/controllers/agent_consent_controller.js +138 -0
- package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
- package/build/src/host/controllers/agent_oauth_controller.js +110 -0
- package/build/src/host/controllers/interaction_controller.js +28 -0
- package/build/src/host/csrf.d.ts +8 -18
- package/build/src/host/csrf.js +28 -2
- package/build/src/host/i18n.d.ts +54 -0
- package/build/src/host/i18n.js +54 -0
- package/build/src/host/impersonation_session.d.ts +5 -0
- package/build/src/host/impersonation_session.js +37 -4
- package/build/src/host/oidc_bearer_guard.js +10 -1
- package/build/src/host/redirect_exact.d.ts +12 -0
- package/build/src/host/redirect_exact.js +17 -0
- package/build/src/host/register_auth_host.js +48 -7
- package/build/src/host/request_url.d.ts +10 -0
- package/build/src/host/request_url.js +17 -0
- package/build/src/host/sudo/methods/magic_link.js +2 -1
- package/build/src/host/sudo/runtime.js +3 -2
- package/build/src/host/sudo_mode.js +4 -4
- package/build/src/mcp/mcp_oauth.d.ts +85 -0
- package/build/src/mcp/mcp_oauth.js +154 -0
- package/build/src/provider/build_provider.js +20 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +11 -0
- package/build/src/provider/token_exchange.d.ts +26 -0
- package/build/src/provider/token_exchange.js +44 -3
- package/build/src/schema/ensure.js +92 -0
- package/build/stubs/ui/react/pages/consent.tsx +17 -1
- package/package.json +1 -1
- 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
|
-
//
|
|
69
|
-
// `
|
|
70
|
-
//
|
|
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
|
-
//
|
|
23
|
-
|
|
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
|
|
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
|
|
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, '&')
|
|
1590
|
+
.replace(/</g, '<')
|
|
1591
|
+
.replace(/>/g, '>')
|
|
1592
|
+
.replace(/"/g, '"')
|
|
1593
|
+
.replace(/'/g, ''');
|
|
1594
|
+
}
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -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;
|
package/build/src/host/csrf.js
CHANGED
|
@@ -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
|
}
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -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;
|