@adonis-agora/authkit-server 0.75.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 +26 -3
- package/build/src/define_config.js +34 -1
- 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/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- 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/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 +15 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +10 -0
- 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,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;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -97,6 +97,11 @@ export const DEFAULT_MESSAGES = {
|
|
|
97
97
|
// raw na view). O nome vem do branding (config-trusted).
|
|
98
98
|
'consent.body': 'The app <strong>{app}</strong> wants to access your account.',
|
|
99
99
|
'consent.submit': 'Authorize',
|
|
100
|
+
'consent.scopes_title': 'It will be able to:',
|
|
101
|
+
'consent.scope.profile': 'See your name and photo',
|
|
102
|
+
'consent.scope.email': 'See your e-mail address',
|
|
103
|
+
'consent.scope.offline_access': 'Stay connected without asking you to sign in again',
|
|
104
|
+
'consent.scope.roles': 'See your roles and organization',
|
|
100
105
|
// Console de conta — login (account/login).
|
|
101
106
|
'account.login.page_title': 'My account',
|
|
102
107
|
'account.login.title': 'My account',
|
|
@@ -183,6 +188,28 @@ export const DEFAULT_MESSAGES = {
|
|
|
183
188
|
'account.apps.revoke': 'Revoke access',
|
|
184
189
|
'account.apps.revoke_confirm': 'Revoke this app’s access? It will need to be authorized again and its tokens will stop working.',
|
|
185
190
|
'account.apps.revoked': 'Access revoked.',
|
|
191
|
+
'account.apps.agents_title': 'Personal agents',
|
|
192
|
+
'account.apps.agents_intro': 'AI assistants you allowed to act on your account, and what each one can do.',
|
|
193
|
+
'account.apps.agents_empty': 'No personal agent can act on your account.',
|
|
194
|
+
'agents.consent.page_title': 'Authorize assistant',
|
|
195
|
+
'agents.consent.title': '{agent} wants to act on your account',
|
|
196
|
+
'agents.consent.body': 'The request came from {origin}. Choose what it can do — you can revoke access at any time.',
|
|
197
|
+
'agents.consent.signed_in_as': 'Signed in as {email}',
|
|
198
|
+
'agents.consent.scopes_label': 'Allow it to:',
|
|
199
|
+
'agents.consent.code': 'Request code: {code}. Make sure it matches the code your assistant shows.',
|
|
200
|
+
'agents.consent.allow': 'Allow',
|
|
201
|
+
'agents.consent.deny': 'Deny',
|
|
202
|
+
'agents.consent.enter_code_title': 'Connect an assistant',
|
|
203
|
+
'agents.consent.enter_code_body': 'Enter the code your assistant showed you.',
|
|
204
|
+
'agents.consent.continue': 'Continue',
|
|
205
|
+
'agents.consent.invalid_code': 'This code is invalid or expired. Ask your assistant for a new one.',
|
|
206
|
+
'agents.consent.impersonating': 'You cannot authorize an assistant while impersonating another account.',
|
|
207
|
+
'agents.done.approved_title': '{agent} can now:',
|
|
208
|
+
'agents.done.denied_title': 'Access denied',
|
|
209
|
+
'agents.done.return': 'You can close this page and return to your assistant.',
|
|
210
|
+
'agents.done.revoke_hint': 'To revoke this access later, go to the apps page of your account.',
|
|
211
|
+
'agents.done.expired_title': 'Request expired',
|
|
212
|
+
'agents.done.expired_body': 'This request is no longer valid. Ask your assistant to start again.',
|
|
186
213
|
'account.apps.not_supported': 'The configured OIDC adapter does not support enumeration — listing apps is unavailable.',
|
|
187
214
|
// Console de conta — organizations (account/orgs).
|
|
188
215
|
'account.orgs.page_title': 'My Organizations',
|
|
@@ -908,6 +935,11 @@ export const PT_BR_MESSAGES = {
|
|
|
908
935
|
'consent.title': 'Autorizar acesso',
|
|
909
936
|
'consent.body': 'O app <strong>{app}</strong> quer acessar sua conta.',
|
|
910
937
|
'consent.submit': 'Autorizar',
|
|
938
|
+
'consent.scopes_title': 'Ele vai poder:',
|
|
939
|
+
'consent.scope.profile': 'Ver seu nome e foto',
|
|
940
|
+
'consent.scope.email': 'Ver seu e-mail',
|
|
941
|
+
'consent.scope.offline_access': 'Continuar conectado sem pedir que você entre de novo',
|
|
942
|
+
'consent.scope.roles': 'Ver seus papéis e sua organização',
|
|
911
943
|
// Console de conta — login (account/login).
|
|
912
944
|
'account.login.page_title': 'Minha conta',
|
|
913
945
|
'account.login.title': 'Minha conta',
|
|
@@ -994,6 +1026,28 @@ export const PT_BR_MESSAGES = {
|
|
|
994
1026
|
'account.apps.revoke': 'Revogar acesso',
|
|
995
1027
|
'account.apps.revoke_confirm': 'Revogar o acesso deste app? Ele precisará ser autorizado novamente e seus tokens deixarão de funcionar.',
|
|
996
1028
|
'account.apps.revoked': 'Acesso revogado.',
|
|
1029
|
+
'account.apps.agents_title': 'Assistentes pessoais',
|
|
1030
|
+
'account.apps.agents_intro': 'Assistentes de IA que você autorizou a agir na sua conta, e o que cada um pode fazer.',
|
|
1031
|
+
'account.apps.agents_empty': 'Nenhum assistente pessoal pode agir na sua conta.',
|
|
1032
|
+
'agents.consent.page_title': 'Autorizar assistente',
|
|
1033
|
+
'agents.consent.title': '{agent} quer agir na sua conta',
|
|
1034
|
+
'agents.consent.body': 'O pedido veio de {origin}. Escolha o que ele pode fazer — você pode revogar o acesso quando quiser.',
|
|
1035
|
+
'agents.consent.signed_in_as': 'Conectado como {email}',
|
|
1036
|
+
'agents.consent.scopes_label': 'Permitir que ele:',
|
|
1037
|
+
'agents.consent.code': 'Código do pedido: {code}. Confira se é o mesmo que o seu assistente mostra.',
|
|
1038
|
+
'agents.consent.allow': 'Permitir',
|
|
1039
|
+
'agents.consent.deny': 'Negar',
|
|
1040
|
+
'agents.consent.enter_code_title': 'Conectar um assistente',
|
|
1041
|
+
'agents.consent.enter_code_body': 'Digite o código que o seu assistente mostrou.',
|
|
1042
|
+
'agents.consent.continue': 'Continuar',
|
|
1043
|
+
'agents.consent.invalid_code': 'Este código é inválido ou expirou. Peça um novo ao seu assistente.',
|
|
1044
|
+
'agents.consent.impersonating': 'Não é possível autorizar um assistente enquanto você personifica outra conta.',
|
|
1045
|
+
'agents.done.approved_title': '{agent} agora pode:',
|
|
1046
|
+
'agents.done.denied_title': 'Acesso negado',
|
|
1047
|
+
'agents.done.return': 'Você pode fechar esta página e voltar ao seu assistente.',
|
|
1048
|
+
'agents.done.revoke_hint': 'Para revogar este acesso depois, vá à página de apps da sua conta.',
|
|
1049
|
+
'agents.done.expired_title': 'Pedido expirado',
|
|
1050
|
+
'agents.done.expired_body': 'Este pedido não vale mais. Peça ao seu assistente para começar de novo.',
|
|
997
1051
|
'account.apps.not_supported': 'O adapter OIDC configurado não suporta enumeração — a listagem de apps fica indisponível.',
|
|
998
1052
|
// Console de conta — organizations (account/orgs).
|
|
999
1053
|
'account.orgs.page_title': 'Minhas Organizações',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redireciona para `url` EXATAMENTE como a lib a montou.
|
|
3
|
+
*
|
|
4
|
+
* O starter do AdonisJS liga `redirect.forwardQueryString: true` no `config/app.ts`: todo
|
|
5
|
+
* `response.redirect(url)` passa a colar a query da request ATUAL no fim — e numa URL que já tem a
|
|
6
|
+
* dela, sai um destino quebrado (`/login?return_to=%2Fx%3Fa%3D1?a=1`). O argumento
|
|
7
|
+
* `forwardQueryString = false` do `redirect()` não desliga o que veio do config; só o
|
|
8
|
+
* `clearQs()` do builder desliga. Toda URL que a lib monta já carrega a query que precisa.
|
|
9
|
+
*
|
|
10
|
+
* Fora de uma Response do AdonisJS (dublês de teste), cai no `redirect(url)` de sempre.
|
|
11
|
+
*/
|
|
12
|
+
export declare function redirectExact(response: any, url: string): unknown;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redireciona para `url` EXATAMENTE como a lib a montou.
|
|
3
|
+
*
|
|
4
|
+
* O starter do AdonisJS liga `redirect.forwardQueryString: true` no `config/app.ts`: todo
|
|
5
|
+
* `response.redirect(url)` passa a colar a query da request ATUAL no fim — e numa URL que já tem a
|
|
6
|
+
* dela, sai um destino quebrado (`/login?return_to=%2Fx%3Fa%3D1?a=1`). O argumento
|
|
7
|
+
* `forwardQueryString = false` do `redirect()` não desliga o que veio do config; só o
|
|
8
|
+
* `clearQs()` do builder desliga. Toda URL que a lib monta já carrega a query que precisa.
|
|
9
|
+
*
|
|
10
|
+
* Fora de uma Response do AdonisJS (dublês de teste), cai no `redirect(url)` de sempre.
|
|
11
|
+
*/
|
|
12
|
+
export function redirectExact(response, url) {
|
|
13
|
+
if (typeof response?.getStatus === 'function') {
|
|
14
|
+
return response.redirect().clearQs().toPath(url);
|
|
15
|
+
}
|
|
16
|
+
return response.redirect(url);
|
|
17
|
+
}
|