@adonis-agora/authkit-server 0.77.0 → 0.78.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 +4 -0
- package/build/host/views/agents/consent.edge +13 -2
- package/build/host/views/agents/done.edge +3 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +8 -0
- package/build/providers/authkit_server_provider.js +15 -1
- package/build/src/agents/config.d.ts +20 -3
- package/build/src/agents/config.js +19 -4
- package/build/src/agents/delegation_service.js +18 -8
- package/build/src/agents/middleware.js +2 -1
- package/build/src/agents/poppy/client_registry.d.ts +45 -0
- package/build/src/agents/poppy/client_registry.js +226 -0
- package/build/src/agents/poppy/config.d.ts +362 -0
- package/build/src/agents/poppy/config.js +201 -0
- package/build/src/agents/poppy/dpop.d.ts +61 -0
- package/build/src/agents/poppy/dpop.js +158 -0
- package/build/src/agents/poppy/errors.d.ts +20 -0
- package/build/src/agents/poppy/errors.js +22 -0
- package/build/src/agents/poppy/replay.d.ts +25 -0
- package/build/src/agents/poppy/replay.js +106 -0
- package/build/src/agents/poppy/resource.d.ts +95 -0
- package/build/src/agents/poppy/resource.js +125 -0
- package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
- package/build/src/agents/poppy/safe_fetch.js +256 -0
- package/build/src/agents/poppy/service.d.ts +280 -0
- package/build/src/agents/poppy/service.js +1452 -0
- package/build/src/agents/poppy/store.d.ts +109 -0
- package/build/src/agents/poppy/store.js +260 -0
- package/build/src/agents/poppy/web_session.d.ts +32 -0
- package/build/src/agents/poppy/web_session.js +104 -0
- package/build/src/agents/protocol.d.ts +8 -4
- package/build/src/agents/runtime.d.ts +17 -1
- package/build/src/agents/runtime.js +138 -13
- package/build/src/define_config.js +6 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -1
- package/build/src/host/account_api/account_api_controller.js +10 -5
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +2 -1
- package/build/src/host/controllers/account_apps_controller.js +13 -3
- package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
- package/build/src/host/controllers/poppy_consent_controller.js +173 -0
- package/build/src/host/controllers/poppy_controller.d.ts +25 -0
- package/build/src/host/controllers/poppy_controller.js +194 -0
- package/build/src/host/csrf.d.ts +7 -0
- package/build/src/host/csrf.js +16 -1
- package/build/src/host/i18n.d.ts +4 -0
- package/build/src/host/i18n.js +4 -0
- package/build/src/host/poppy_session_key.d.ts +9 -0
- package/build/src/host/poppy_session_key.js +9 -0
- package/build/src/host/register_auth_host.js +60 -0
- package/build/src/provider/oidc_service.d.ts +5 -0
- package/build/src/provider/oidc_service.js +9 -2
- package/build/src/schema/ensure.js +124 -0
- package/package.json +1 -1
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
2
|
+
import { findMcpResource } from '../mcp/mcp_oauth.js';
|
|
2
3
|
import { PersonalAgentVerifier } from './agent_identity.js';
|
|
3
4
|
import { PersonalAgentDelegation } from './delegation_service.js';
|
|
4
5
|
import { DelegationStore } from './delegation_store.js';
|
|
6
|
+
import { PoppyClientRegistry } from './poppy/client_registry.js';
|
|
7
|
+
import { hmacNonceSource } from './poppy/dpop.js';
|
|
8
|
+
import { databaseReplayStore, memoryLimiter, memoryReplayStore, redisReplayStore, } from './poppy/replay.js';
|
|
9
|
+
import { PoppyService, poppyUrls } from './poppy/service.js';
|
|
10
|
+
import { PoppyStore } from './poppy/store.js';
|
|
5
11
|
import { keystoreSigner } from './signer.js';
|
|
6
12
|
/**
|
|
7
13
|
* Deriva as URLs da ORIGEM do issuer configurado — nunca do Host da request
|
|
@@ -20,11 +26,77 @@ export function personalAgentUrls(oidcIssuer, prefix) {
|
|
|
20
26
|
};
|
|
21
27
|
}
|
|
22
28
|
const runtimes = new WeakMap();
|
|
29
|
+
/** Ponte Poppy → oidc-provider: Bearer de MCP de Sessions logadas viram access tokens dele. */
|
|
30
|
+
function mcpBridge(service, poppyApis) {
|
|
31
|
+
return {
|
|
32
|
+
isMcpResource(url) {
|
|
33
|
+
try {
|
|
34
|
+
return findMcpResource(url, service.config.issuer, service.config.mcp) !== null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async mint(input) {
|
|
41
|
+
const provider = service.provider;
|
|
42
|
+
if (!provider?.AccessToken || !provider?.Grant)
|
|
43
|
+
return null;
|
|
44
|
+
// Só faz sentido quando a integração MCP do authkit está ligada, ou o MCP é de `apis`.
|
|
45
|
+
if (!service.config.mcp?.enabled &&
|
|
46
|
+
!poppyApis.some((a) => a.type === 'mcp' && a.url.replace(/\/+$/, '') === input.resource.replace(/\/+$/, ''))) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const remaining = Math.max(60, Math.floor((input.grantExpiresAt.getTime() - Date.now()) / 1000));
|
|
50
|
+
let grant = await provider.Grant.find(input.grantId);
|
|
51
|
+
if (!grant) {
|
|
52
|
+
grant = new provider.Grant({
|
|
53
|
+
jti: input.grantId,
|
|
54
|
+
accountId: input.accountId,
|
|
55
|
+
clientId: input.clientId,
|
|
56
|
+
expiresIn: remaining,
|
|
57
|
+
});
|
|
58
|
+
await grant.save();
|
|
59
|
+
}
|
|
60
|
+
const at = new provider.AccessToken({
|
|
61
|
+
accountId: input.accountId,
|
|
62
|
+
clientId: input.clientId,
|
|
63
|
+
grantId: input.grantId,
|
|
64
|
+
scope: input.scope,
|
|
65
|
+
gty: 'poppy',
|
|
66
|
+
expiresIn: input.expiresIn,
|
|
67
|
+
});
|
|
68
|
+
at.aud = input.resource;
|
|
69
|
+
return at.save();
|
|
70
|
+
},
|
|
71
|
+
async revoke(grantId) {
|
|
72
|
+
const provider = service.provider;
|
|
73
|
+
if (!provider?.Grant)
|
|
74
|
+
return;
|
|
75
|
+
await provider.AccessToken?.revokeByGrantId?.(grantId);
|
|
76
|
+
const grant = await provider.Grant.find(grantId);
|
|
77
|
+
if (grant)
|
|
78
|
+
await grant.destroy();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function resolveReplay(replay, conn, make) {
|
|
83
|
+
if (replay === 'memory')
|
|
84
|
+
return memoryReplayStore();
|
|
85
|
+
if (replay === 'database')
|
|
86
|
+
return databaseReplayStore(conn);
|
|
87
|
+
if (typeof replay.claim === 'function')
|
|
88
|
+
return replay;
|
|
89
|
+
const name = replay.redis;
|
|
90
|
+
if (!make)
|
|
91
|
+
throw new Error('authkit: personalAgents.poppy.replay { redis } exige o container.');
|
|
92
|
+
const redis = await make('redis');
|
|
93
|
+
return redisReplayStore(redis.connection(name));
|
|
94
|
+
}
|
|
23
95
|
/**
|
|
24
96
|
* Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
|
|
25
97
|
* `personalAgents` não está no config — as rotas respondem 404.
|
|
26
98
|
*/
|
|
27
|
-
export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
99
|
+
export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
|
|
28
100
|
let runtime = runtimes.get(service);
|
|
29
101
|
if (!runtime) {
|
|
30
102
|
runtime = (async () => {
|
|
@@ -32,25 +104,76 @@ export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
|
32
104
|
if (!config)
|
|
33
105
|
return null;
|
|
34
106
|
const urls = personalAgentUrls(service.config.issuer, config.prefix);
|
|
35
|
-
|
|
36
|
-
|
|
107
|
+
// Conta apagada ou suspensa corta a delegação na hora.
|
|
108
|
+
const isAccountActive = async (accountId) => {
|
|
109
|
+
const store = service.config.accountStore;
|
|
110
|
+
if (!(await store.findById(accountId)))
|
|
111
|
+
return false;
|
|
112
|
+
return !(supportsAccountStatus(store) && (await store.isDisabled(accountId)));
|
|
113
|
+
};
|
|
114
|
+
let conn = null;
|
|
115
|
+
if (config.delegation || config.poppy) {
|
|
37
116
|
const db = await makeDb();
|
|
38
117
|
const connection = service.config.schema.connection;
|
|
118
|
+
conn = () => (connection ? db.connection(connection) : db.connection());
|
|
119
|
+
}
|
|
120
|
+
const store = conn ? new DelegationStore(conn) : null;
|
|
121
|
+
let delegation = null;
|
|
122
|
+
if (config.delegation && store) {
|
|
39
123
|
delegation = new PersonalAgentDelegation({
|
|
40
124
|
cfg: config.delegation,
|
|
41
|
-
store
|
|
125
|
+
store,
|
|
42
126
|
signer: keystoreSigner(service),
|
|
43
127
|
urls: { issuer: urls.issuer, consent: urls.consent },
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
128
|
+
isAccountActive,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
let poppy = null;
|
|
132
|
+
if (config.poppy && store && conn) {
|
|
133
|
+
const cfg = config.poppy;
|
|
134
|
+
const limiter = cfg.rateLimit === false
|
|
135
|
+
? null
|
|
136
|
+
: typeof cfg.rateLimit === 'function'
|
|
137
|
+
? cfg.rateLimit
|
|
138
|
+
: memoryLimiter(cfg.rateLimit);
|
|
139
|
+
const secret = service.deriveSecret('poppy-dpop-nonce');
|
|
140
|
+
poppy = new PoppyService({
|
|
141
|
+
cfg,
|
|
142
|
+
urls: poppyUrls(service.config.issuer, cfg.prefix),
|
|
143
|
+
store: new PoppyStore(conn),
|
|
144
|
+
grants: store,
|
|
145
|
+
registry: new PoppyClientRegistry(cfg, { fetchJson: options.fetchJson }),
|
|
146
|
+
replay: await resolveReplay(cfg.replay, conn, options.make),
|
|
147
|
+
limiter,
|
|
148
|
+
nonces: hmacNonceSource(secret),
|
|
149
|
+
isAccountActive,
|
|
150
|
+
mcp: mcpBridge(service, cfg.apis),
|
|
151
|
+
now: options.now,
|
|
51
152
|
});
|
|
52
153
|
}
|
|
53
|
-
|
|
154
|
+
const revokeAgentGrant = async (accountId, grantId) => {
|
|
155
|
+
if (grantId.startsWith('pgrant_')) {
|
|
156
|
+
return poppy ? poppy.revokeAccountGrant(accountId, grantId) : false;
|
|
157
|
+
}
|
|
158
|
+
return delegation ? delegation.revokeGrant(accountId, grantId) : false;
|
|
159
|
+
};
|
|
160
|
+
const revokeAllAgentGrants = async (accountId) => {
|
|
161
|
+
let count = 0;
|
|
162
|
+
if (poppy)
|
|
163
|
+
count += await poppy.revokeAllAccountGrants(accountId);
|
|
164
|
+
if (delegation)
|
|
165
|
+
count += await delegation.revokeAllGrants(accountId);
|
|
166
|
+
return count;
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
config,
|
|
170
|
+
urls,
|
|
171
|
+
verifier: new PersonalAgentVerifier(config),
|
|
172
|
+
delegation,
|
|
173
|
+
poppy,
|
|
174
|
+
revokeAgentGrant,
|
|
175
|
+
revokeAllAgentGrants,
|
|
176
|
+
};
|
|
54
177
|
})();
|
|
55
178
|
// Falha (DB indisponível) não fica em cache — a próxima request tenta de novo.
|
|
56
179
|
runtime.catch(() => runtimes.delete(service));
|
|
@@ -61,5 +184,7 @@ export function buildPersonalAgentsRuntime(service, makeDb) {
|
|
|
61
184
|
/** Runtime a partir de uma request. */
|
|
62
185
|
export async function personalAgentsFor(ctx) {
|
|
63
186
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
64
|
-
return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db')
|
|
187
|
+
return buildPersonalAgentsRuntime(service, () => ctx.containerResolver.make('lucid.db'), {
|
|
188
|
+
make: (binding) => ctx.containerResolver.make(binding),
|
|
189
|
+
});
|
|
65
190
|
}
|
|
@@ -353,6 +353,12 @@ export function defineConfig(config) {
|
|
|
353
353
|
(personalAgents.prefix === oidcMount || personalAgents.prefix.startsWith(`${oidcMount}/`))) {
|
|
354
354
|
throw new Error(`authkit: personalAgents.prefix "${personalAgents.prefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
355
355
|
}
|
|
356
|
+
const poppyPrefix = personalAgents?.poppy?.prefix;
|
|
357
|
+
if (poppyPrefix &&
|
|
358
|
+
oidcMount &&
|
|
359
|
+
(poppyPrefix === oidcMount || poppyPrefix.startsWith(`${oidcMount}/`))) {
|
|
360
|
+
throw new Error(`authkit: personalAgents.poppy.prefix "${poppyPrefix}" não pode ficar debaixo do mountPath do OIDC ("${oidcMount}").`);
|
|
361
|
+
}
|
|
356
362
|
// BACKSTOP DE SUDO. Um host cujo `sudo.methods` não tem um único método
|
|
357
363
|
// satisfazível por conta sem senha fica bricado para TODA operação sob
|
|
358
364
|
// `requireSudo` — e hoje isso só se descobre quando um usuário não consegue
|
|
@@ -159,7 +159,18 @@ export default class AccountApiController {
|
|
|
159
159
|
}>;
|
|
160
160
|
/** Personal agents com delegação ativa na conta (scopes aprovados). */
|
|
161
161
|
listAgents(ctx: HttpContext): Promise<void | {
|
|
162
|
-
agents:
|
|
162
|
+
agents: {
|
|
163
|
+
id: string;
|
|
164
|
+
clientId: string;
|
|
165
|
+
scopes: {
|
|
166
|
+
id: string;
|
|
167
|
+
description: string;
|
|
168
|
+
}[];
|
|
169
|
+
createdAt: Date;
|
|
170
|
+
updatedAt: Date;
|
|
171
|
+
expiresAt: Date;
|
|
172
|
+
protocol: string;
|
|
173
|
+
}[];
|
|
163
174
|
}>;
|
|
164
175
|
/** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
|
|
165
176
|
revokeAgent(ctx: HttpContext): Promise<void | {
|
|
@@ -617,20 +617,25 @@ export default class AccountApiController {
|
|
|
617
617
|
/** Personal agents com delegação ativa na conta (scopes aprovados). */
|
|
618
618
|
async listAgents(ctx) {
|
|
619
619
|
const runtime = await personalAgentsFor(ctx);
|
|
620
|
-
if (!runtime?.delegation) {
|
|
620
|
+
if (!runtime?.delegation && !runtime?.poppy) {
|
|
621
621
|
return ctx.response
|
|
622
622
|
.status(404)
|
|
623
623
|
.send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
|
|
624
624
|
}
|
|
625
625
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
626
|
-
const
|
|
627
|
-
|
|
626
|
+
const pact = runtime.delegation
|
|
627
|
+
? (await runtime.delegation.listGrants(userId)).map((g) => ({ ...g, protocol: 'pact' }))
|
|
628
|
+
: [];
|
|
629
|
+
const poppy = runtime.poppy
|
|
630
|
+
? (await runtime.poppy.listGrants(userId)).map((g) => ({ ...g, protocol: 'poppy' }))
|
|
631
|
+
: [];
|
|
632
|
+
return { agents: [...pact, ...poppy] };
|
|
628
633
|
}
|
|
629
634
|
// ─── DELETE /account/api/agents/:id ──────────────────────────────────────
|
|
630
635
|
/** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
|
|
631
636
|
async revokeAgent(ctx) {
|
|
632
637
|
const runtime = await personalAgentsFor(ctx);
|
|
633
|
-
if (!runtime?.delegation) {
|
|
638
|
+
if (!runtime?.delegation && !runtime?.poppy) {
|
|
634
639
|
return ctx.response
|
|
635
640
|
.status(404)
|
|
636
641
|
.send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
|
|
@@ -638,7 +643,7 @@ export default class AccountApiController {
|
|
|
638
643
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
639
644
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
640
645
|
const grantId = ctx.request.param('id');
|
|
641
|
-
if (!(await runtime.
|
|
646
|
+
if (!(await runtime.revokeAgentGrant(userId, grantId))) {
|
|
642
647
|
return ctx.response.status(404).send(apiErr('not_found', 'Grant not found.'));
|
|
643
648
|
}
|
|
644
649
|
await service.config.audit?.record({
|
|
@@ -87,13 +87,14 @@ export class AdminSessionsService {
|
|
|
87
87
|
/**
|
|
88
88
|
* Revogação total também corta os personal agents: "sair de todas as sessões",
|
|
89
89
|
* reset de senha, a ação do admin e a exclusão de conta não podem deixar um
|
|
90
|
-
* agente agindo na conta
|
|
91
|
-
* resolve o banco. Diferente do
|
|
92
|
-
* não há outra camada que corte
|
|
90
|
+
* agente agindo na conta (delegações do PACT e Account Tokens do Poppy). Sem
|
|
91
|
+
* `personalAgents` é no-op — e aí nem resolve o banco. Diferente do
|
|
92
|
+
* `recordSubRevocation`, uma falha AQUI propaga: não há outra camada que corte
|
|
93
|
+
* esses tokens.
|
|
93
94
|
*/
|
|
94
95
|
async #revokeAgentGrants(accountId) {
|
|
95
96
|
const runtime = await buildPersonalAgentsRuntime(this.#oidc, () => getBootedApp().container.make('lucid.db'));
|
|
96
|
-
return runtime
|
|
97
|
+
return runtime ? runtime.revokeAllAgentGrants(accountId) : 0;
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
99
100
|
* Loga (best-effort, sem nunca lançar) a falha de {@link recordSubRevocation}. Prefere o
|
|
@@ -183,7 +184,9 @@ export class AdminSessionsService {
|
|
|
183
184
|
*/
|
|
184
185
|
async listGrants(accountId) {
|
|
185
186
|
const rows = await this.#listModel('Grant');
|
|
186
|
-
|
|
187
|
+
// Os grants `pgrant_` são o espelho de um Account Token do Poppy (Bearer de MCP) — aparecem e
|
|
188
|
+
// são revogados como personal agent, não como app OIDC.
|
|
189
|
+
const grants = rows.filter((r) => r.payload.accountId === accountId && !r.id.startsWith('pgrant_'));
|
|
187
190
|
if (grants.length === 0)
|
|
188
191
|
return [];
|
|
189
192
|
const atByGrant = await this.#countByGrant('AccessToken');
|
|
@@ -35,9 +35,10 @@ export interface AuthHostRuntimeConfig {
|
|
|
35
35
|
* Ver `deriveLockedRouteOptions`.
|
|
36
36
|
*/
|
|
37
37
|
lockedRouteOptions?: PolicyRouteOption[];
|
|
38
|
-
/** Personal agents ligados → prefixo das rotas deles (`/agents`). */
|
|
38
|
+
/** Personal agents ligados → prefixo das rotas deles (`/agents`) e do Poppy (`/poppy`). */
|
|
39
39
|
personalAgents?: {
|
|
40
40
|
prefix: string;
|
|
41
|
+
poppyPrefix?: string;
|
|
41
42
|
};
|
|
42
43
|
/**
|
|
43
44
|
* API headless (Clerk-style). Presente quando o host a declarou — o provider o
|
|
@@ -28,7 +28,7 @@ export default class AccountAppsController {
|
|
|
28
28
|
const revoked = ctx.session.flashMessages.get('appRevoked');
|
|
29
29
|
// Personal agents com delegação ativa (só quando a feature está ligada).
|
|
30
30
|
const agentsRuntime = await personalAgentsFor(ctx);
|
|
31
|
-
const
|
|
31
|
+
const pactAgents = agentsRuntime?.delegation
|
|
32
32
|
? await Promise.all((await agentsRuntime.delegation.listGrants(accountId)).map(async (g) => ({
|
|
33
33
|
id: g.id,
|
|
34
34
|
name: displayNameFor((await agentsRuntime.config.resolveAgent(g.clientId)) ?? {
|
|
@@ -38,6 +38,16 @@ export default class AccountAppsController {
|
|
|
38
38
|
scopes: g.scopes,
|
|
39
39
|
})))
|
|
40
40
|
: null;
|
|
41
|
+
// Account Tokens do Poppy: o agente é desconectado pela mesma lista (§4.9).
|
|
42
|
+
const poppy = agentsRuntime?.poppy;
|
|
43
|
+
const poppyAgents = poppy
|
|
44
|
+
? await Promise.all((await poppy.listGrants(accountId)).map(async (g) => ({
|
|
45
|
+
id: g.id,
|
|
46
|
+
name: await poppy.registry.displayName(g.clientId),
|
|
47
|
+
scopes: g.scopes,
|
|
48
|
+
})))
|
|
49
|
+
: null;
|
|
50
|
+
const agents = pactAgents || poppyAgents ? [...(pactAgents ?? []), ...(poppyAgents ?? [])] : null;
|
|
41
51
|
return render(ctx, 'account/apps', {
|
|
42
52
|
csrfToken: ctx.request.csrfToken,
|
|
43
53
|
supported,
|
|
@@ -78,13 +88,13 @@ export default class AccountAppsController {
|
|
|
78
88
|
/** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
|
|
79
89
|
async revokeAgent(ctx) {
|
|
80
90
|
const runtime = await personalAgentsFor(ctx);
|
|
81
|
-
if (!runtime?.delegation)
|
|
91
|
+
if (!runtime?.delegation && !runtime?.poppy)
|
|
82
92
|
return ctx.response.notFound();
|
|
83
93
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
84
94
|
const cfg = service.config;
|
|
85
95
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
86
96
|
const grantId = ctx.request.param('grantId');
|
|
87
|
-
if (await runtime.
|
|
97
|
+
if (await runtime.revokeAgentGrant(accountId, grantId)) {
|
|
88
98
|
await cfg.audit?.record({
|
|
89
99
|
type: 'agent.grant_revoked',
|
|
90
100
|
accountId,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Telas do usuário no Poppy: o consentimento do Direct Sign-In (o
|
|
5
|
+
* `authorization_endpoint`, §4.5) e a do Device Sign-In (o `verification_uri`,
|
|
6
|
+
* §4.6). Montadas atrás do `accountGuard`: sem sessão, o usuário passa pelo login
|
|
7
|
+
* DESTE app e volta. Abrir o link nunca aprova sozinho — a aprovação é um POST
|
|
8
|
+
* com CSRF. Recusadas durante impersonation.
|
|
9
|
+
*/
|
|
10
|
+
export default class PoppyConsentController {
|
|
11
|
+
/** GET {prefix}/oauth/authorize */
|
|
12
|
+
authorize(ctx: HttpContext): Promise<any>;
|
|
13
|
+
/** POST {prefix}/oauth/authorize — `request_id`, `decision=allow|deny`, `scope[]`. */
|
|
14
|
+
decideAuthorization(ctx: HttpContext): Promise<any>;
|
|
15
|
+
/** GET {prefix}/device?user_code=XXXX-XXXX */
|
|
16
|
+
device(ctx: HttpContext): Promise<any>;
|
|
17
|
+
/** POST {prefix}/device — `user_code`, `decision`, `scope[]`. */
|
|
18
|
+
decideDevice(ctx: HttpContext): Promise<any>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { personalAgentsFor } from '../../agents/runtime.js';
|
|
3
|
+
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
4
|
+
import { impersonationState } from '../impersonation_session.js';
|
|
5
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
6
|
+
/** A tela nunca pode ser emoldurada (clickjacking — ver `agent_consent_controller.ts`). */
|
|
7
|
+
function denyFraming(ctx) {
|
|
8
|
+
ctx.response.header('X-Frame-Options', 'DENY');
|
|
9
|
+
ctx.response.header('Content-Security-Policy', "frame-ancestors 'none'");
|
|
10
|
+
}
|
|
11
|
+
function chosenScopes(ctx) {
|
|
12
|
+
const raw = ctx.request.input('scope');
|
|
13
|
+
return (Array.isArray(raw) ? raw : raw ? [raw] : []).map(String);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Telas do usuário no Poppy: o consentimento do Direct Sign-In (o
|
|
17
|
+
* `authorization_endpoint`, §4.5) e a do Device Sign-In (o `verification_uri`,
|
|
18
|
+
* §4.6). Montadas atrás do `accountGuard`: sem sessão, o usuário passa pelo login
|
|
19
|
+
* DESTE app e volta. Abrir o link nunca aprova sozinho — a aprovação é um POST
|
|
20
|
+
* com CSRF. Recusadas durante impersonation.
|
|
21
|
+
*/
|
|
22
|
+
export default class PoppyConsentController {
|
|
23
|
+
/** GET {prefix}/oauth/authorize */
|
|
24
|
+
async authorize(ctx) {
|
|
25
|
+
const runtime = await personalAgentsFor(ctx);
|
|
26
|
+
const poppy = runtime?.poppy;
|
|
27
|
+
if (!poppy?.cfg.signIn.direct)
|
|
28
|
+
return ctx.response.notFound();
|
|
29
|
+
denyFraming(ctx);
|
|
30
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
31
|
+
const render = service.config.render;
|
|
32
|
+
if (impersonationState(ctx).active) {
|
|
33
|
+
return render(ctx, 'agents/consent', {
|
|
34
|
+
csrfToken: ctx.request.csrfToken,
|
|
35
|
+
action: poppy.urls.authorization,
|
|
36
|
+
request: null,
|
|
37
|
+
account: null,
|
|
38
|
+
error: 'agents.consent.impersonating',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
42
|
+
const outcome = await poppy.startAuthorization(ctx.request.qs(), accountId);
|
|
43
|
+
if (outcome.kind === 'redirect')
|
|
44
|
+
return redirectExact(ctx.response, outcome.url);
|
|
45
|
+
if (outcome.kind === 'error') {
|
|
46
|
+
ctx.response.status(400);
|
|
47
|
+
return render(ctx, 'agents/done', { status: 'invalid', agentName: null, scopes: [] });
|
|
48
|
+
}
|
|
49
|
+
const account = await service.config.accountStore.findById(accountId);
|
|
50
|
+
return render(ctx, 'agents/consent', {
|
|
51
|
+
csrfToken: ctx.request.csrfToken,
|
|
52
|
+
action: poppy.urls.authorization,
|
|
53
|
+
request: {
|
|
54
|
+
agentName: outcome.request.agentName,
|
|
55
|
+
agentOrigin: outcome.request.agentOrigin,
|
|
56
|
+
logoUri: outcome.request.logoUri,
|
|
57
|
+
scopes: outcome.request.scopes,
|
|
58
|
+
hidden: [{ name: 'request_id', value: outcome.request.id }],
|
|
59
|
+
},
|
|
60
|
+
account: { email: account?.email ?? null },
|
|
61
|
+
error: null,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** POST {prefix}/oauth/authorize — `request_id`, `decision=allow|deny`, `scope[]`. */
|
|
65
|
+
async decideAuthorization(ctx) {
|
|
66
|
+
const runtime = await personalAgentsFor(ctx);
|
|
67
|
+
const poppy = runtime?.poppy;
|
|
68
|
+
if (!poppy?.cfg.signIn.direct)
|
|
69
|
+
return ctx.response.notFound();
|
|
70
|
+
denyFraming(ctx);
|
|
71
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
72
|
+
const cfg = service.config;
|
|
73
|
+
const render = cfg.render;
|
|
74
|
+
if (impersonationState(ctx).active) {
|
|
75
|
+
return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
|
|
76
|
+
}
|
|
77
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
78
|
+
const decided = await poppy.decideAuthorization({
|
|
79
|
+
requestId: String(ctx.request.input('request_id') ?? ''),
|
|
80
|
+
accountId,
|
|
81
|
+
allow: ctx.request.input('decision') === 'allow',
|
|
82
|
+
scopes: chosenScopes(ctx),
|
|
83
|
+
});
|
|
84
|
+
if (!decided) {
|
|
85
|
+
return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
|
|
86
|
+
}
|
|
87
|
+
await cfg.audit?.record({
|
|
88
|
+
type: decided.approved ? 'agent.delegation_approved' : 'agent.delegation_denied',
|
|
89
|
+
accountId,
|
|
90
|
+
clientId: decided.clientId,
|
|
91
|
+
ip: ctx.request.ip?.() ?? null,
|
|
92
|
+
metadata: { protocol: 'poppy', signIn: 'direct', scopes: decided.approved ?? [] },
|
|
93
|
+
});
|
|
94
|
+
return redirectExact(ctx.response, decided.url);
|
|
95
|
+
}
|
|
96
|
+
/** GET {prefix}/device?user_code=XXXX-XXXX */
|
|
97
|
+
async device(ctx) {
|
|
98
|
+
const runtime = await personalAgentsFor(ctx);
|
|
99
|
+
const poppy = runtime?.poppy;
|
|
100
|
+
if (!poppy?.cfg.signIn.device)
|
|
101
|
+
return ctx.response.notFound();
|
|
102
|
+
denyFraming(ctx);
|
|
103
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
104
|
+
const render = service.config.render;
|
|
105
|
+
const base = {
|
|
106
|
+
csrfToken: ctx.request.csrfToken,
|
|
107
|
+
action: poppy.urls.device,
|
|
108
|
+
request: null,
|
|
109
|
+
account: null,
|
|
110
|
+
error: null,
|
|
111
|
+
};
|
|
112
|
+
if (impersonationState(ctx).active) {
|
|
113
|
+
return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.impersonating' });
|
|
114
|
+
}
|
|
115
|
+
const input = ctx.request.input('user_code');
|
|
116
|
+
if (!input)
|
|
117
|
+
return render(ctx, 'agents/consent', base);
|
|
118
|
+
const pending = await poppy.pendingDevice(input);
|
|
119
|
+
if (!pending) {
|
|
120
|
+
return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.invalid_code' });
|
|
121
|
+
}
|
|
122
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
123
|
+
const account = await service.config.accountStore.findById(accountId);
|
|
124
|
+
return render(ctx, 'agents/consent', {
|
|
125
|
+
...base,
|
|
126
|
+
request: {
|
|
127
|
+
userCode: pending.userCode,
|
|
128
|
+
agentName: pending.agentName,
|
|
129
|
+
agentOrigin: pending.agentOrigin,
|
|
130
|
+
scopes: pending.scopes,
|
|
131
|
+
},
|
|
132
|
+
account: { email: account?.email ?? null },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** POST {prefix}/device — `user_code`, `decision`, `scope[]`. */
|
|
136
|
+
async decideDevice(ctx) {
|
|
137
|
+
const runtime = await personalAgentsFor(ctx);
|
|
138
|
+
const poppy = runtime?.poppy;
|
|
139
|
+
if (!poppy?.cfg.signIn.device)
|
|
140
|
+
return ctx.response.notFound();
|
|
141
|
+
denyFraming(ctx);
|
|
142
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
143
|
+
const cfg = service.config;
|
|
144
|
+
const render = cfg.render;
|
|
145
|
+
if (impersonationState(ctx).active) {
|
|
146
|
+
return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
|
|
147
|
+
}
|
|
148
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
149
|
+
const userCode = String(ctx.request.input('user_code') ?? '');
|
|
150
|
+
const pending = await poppy.pendingDevice(userCode);
|
|
151
|
+
const decided = await poppy.decideDevice({
|
|
152
|
+
userCode,
|
|
153
|
+
accountId,
|
|
154
|
+
allow: ctx.request.input('decision') === 'allow',
|
|
155
|
+
scopes: chosenScopes(ctx),
|
|
156
|
+
});
|
|
157
|
+
if (!decided || !pending) {
|
|
158
|
+
return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
|
|
159
|
+
}
|
|
160
|
+
await cfg.audit?.record({
|
|
161
|
+
type: decided.approved ? 'agent.delegation_approved' : 'agent.delegation_denied',
|
|
162
|
+
accountId,
|
|
163
|
+
clientId: decided.clientId,
|
|
164
|
+
ip: ctx.request.ip?.() ?? null,
|
|
165
|
+
metadata: { protocol: 'poppy', signIn: 'device', scopes: decided.approved ?? [] },
|
|
166
|
+
});
|
|
167
|
+
return render(ctx, 'agents/done', {
|
|
168
|
+
status: decided.approved ? 'approved' : 'denied',
|
|
169
|
+
agentName: pending.agentName,
|
|
170
|
+
scopes: decided.approved ? poppy.describe(decided.approved) : [],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* Endpoints server-to-server do Poppy (§3, §4): descoberta, metadata RFC 8414,
|
|
4
|
+
* token, revogação, device authorization, Mediated Sign-In e a sessão de
|
|
5
|
+
* navegador (§5). Todos isentos de CSRF — ver `authkitCsrfExceptions`.
|
|
6
|
+
*/
|
|
7
|
+
export default class PoppyController {
|
|
8
|
+
/** `GET /.well-known/poppy.json` — só no domínio da empresa (ignorando `www.`). */
|
|
9
|
+
discovery(ctx: HttpContext): Promise<void>;
|
|
10
|
+
/** `GET /.well-known/oauth-authorization-server{prefix}` (RFC 8414 + `poppy_domains`). */
|
|
11
|
+
metadata(ctx: HttpContext): Promise<void>;
|
|
12
|
+
token(ctx: HttpContext): Promise<void>;
|
|
13
|
+
deviceAuthorization(ctx: HttpContext): Promise<void>;
|
|
14
|
+
/** RFC 7009: 200 também para token desconhecido. */
|
|
15
|
+
revoke(ctx: HttpContext): Promise<void>;
|
|
16
|
+
/** `POST auth.mediated.endpoint` (§4.7) — com o Session Token (DPoP). */
|
|
17
|
+
mediatedStart(ctx: HttpContext): Promise<void>;
|
|
18
|
+
/** `POST {mediated}/{sign_in_id}` (§4.7). */
|
|
19
|
+
mediatedCode(ctx: HttpContext): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* `POST web.browser_session_endpoint` (§5): asserção no corpo → cookie do app
|
|
22
|
+
* para a Session e 303 para `return_to`; qualquer falha → 400, sem cookie.
|
|
23
|
+
*/
|
|
24
|
+
browserSession(ctx: HttpContext): Promise<void>;
|
|
25
|
+
}
|