@adonis-agora/authkit-server 0.77.0 → 0.79.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 +22 -3
- package/build/src/agents/config.js +22 -4
- package/build/src/agents/delegation_service.js +18 -8
- package/build/src/agents/delegation_store.d.ts +16 -0
- package/build/src/agents/delegation_store.js +44 -0
- 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 +365 -0
- package/build/src/agents/poppy/config.js +208 -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 +286 -0
- package/build/src/agents/poppy/service.js +1464 -0
- package/build/src/agents/poppy/store.d.ts +111 -0
- package/build/src/agents/poppy/store.js +269 -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/registry_store.d.ts +41 -0
- package/build/src/agents/registry_store.js +138 -0
- package/build/src/agents/runtime.d.ts +30 -1
- package/build/src/agents/runtime.js +221 -14
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +6 -0
- 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_api/dto.d.ts +1 -1
- package/build/src/host/admin_console/admin_shell_controller.js +7 -0
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +83 -0
- package/build/src/host/admin_console/console_personal_agents_controller.js +337 -0
- 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 +76 -0
- package/build/src/host/ui-dist/assets/index-LrScaR9C.js +155 -0
- package/build/src/host/ui-dist/index.html +1 -1
- 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 +152 -0
- package/package.json +2 -2
- package/build/src/host/ui-dist/assets/index-CIqtoNcO.js +0 -155
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { PoppyError } from '../../agents/poppy/errors.js';
|
|
2
|
+
import { publicRequestUrl, writePoppyAuthError } from '../../agents/poppy/resource.js';
|
|
3
|
+
import { loginPoppyBrowser } from '../../agents/poppy/web_session.js';
|
|
4
|
+
import { personalAgentsFor } from '../../agents/runtime.js';
|
|
5
|
+
function noStore(ctx) {
|
|
6
|
+
ctx.response.header('Cache-Control', 'no-store');
|
|
7
|
+
ctx.response.header('Pragma', 'no-cache');
|
|
8
|
+
}
|
|
9
|
+
function withNonce(ctx, poppy) {
|
|
10
|
+
const nonce = poppy.nonce();
|
|
11
|
+
if (nonce)
|
|
12
|
+
ctx.response.header('DPoP-Nonce', nonce);
|
|
13
|
+
}
|
|
14
|
+
function oauthError(ctx, error) {
|
|
15
|
+
noStore(ctx);
|
|
16
|
+
for (const [k, v] of Object.entries(error.headers))
|
|
17
|
+
ctx.response.header(k, v);
|
|
18
|
+
if (error.code === 'invalid_dpop_proof' || error.code === 'use_dpop_nonce') {
|
|
19
|
+
ctx.response.header('WWW-Authenticate', `DPoP error="${error.code}"`);
|
|
20
|
+
}
|
|
21
|
+
return ctx.response.status(error.status).json(error.toJSON());
|
|
22
|
+
}
|
|
23
|
+
async function poppyOr404(ctx) {
|
|
24
|
+
const poppy = (await personalAgentsFor(ctx))?.poppy ?? null;
|
|
25
|
+
if (!poppy)
|
|
26
|
+
ctx.response.notFound();
|
|
27
|
+
return poppy;
|
|
28
|
+
}
|
|
29
|
+
/** Formulário (`application/x-www-form-urlencoded`) da request. */
|
|
30
|
+
function form(ctx) {
|
|
31
|
+
return ctx.request.all();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Endpoints server-to-server do Poppy (§3, §4): descoberta, metadata RFC 8414,
|
|
35
|
+
* token, revogação, device authorization, Mediated Sign-In e a sessão de
|
|
36
|
+
* navegador (§5). Todos isentos de CSRF — ver `authkitCsrfExceptions`.
|
|
37
|
+
*/
|
|
38
|
+
export default class PoppyController {
|
|
39
|
+
/** `GET /.well-known/poppy.json` — só no domínio da empresa (ignorando `www.`). */
|
|
40
|
+
async discovery(ctx) {
|
|
41
|
+
const poppy = await poppyOr404(ctx);
|
|
42
|
+
if (!poppy)
|
|
43
|
+
return;
|
|
44
|
+
// `host()` mantém a porta (dev em `localhost:3333`): o `browser_session_endpoint` sai dele.
|
|
45
|
+
const doc = poppy.discoveryDocument(ctx.request.host() ?? ctx.request.hostname() ?? '');
|
|
46
|
+
if (!doc)
|
|
47
|
+
return ctx.response.notFound();
|
|
48
|
+
ctx.response.header('Access-Control-Allow-Origin', '*');
|
|
49
|
+
ctx.response.header('Cache-Control', 'public, max-age=300');
|
|
50
|
+
return ctx.response.json(doc);
|
|
51
|
+
}
|
|
52
|
+
/** `GET /.well-known/oauth-authorization-server{prefix}` (RFC 8414 + `poppy_domains`). */
|
|
53
|
+
async metadata(ctx) {
|
|
54
|
+
const poppy = await poppyOr404(ctx);
|
|
55
|
+
if (!poppy)
|
|
56
|
+
return;
|
|
57
|
+
ctx.response.header('Access-Control-Allow-Origin', '*');
|
|
58
|
+
ctx.response.header('Cache-Control', 'public, max-age=300');
|
|
59
|
+
return ctx.response.json(poppy.authorizationServerMetadata());
|
|
60
|
+
}
|
|
61
|
+
async token(ctx) {
|
|
62
|
+
const poppy = await poppyOr404(ctx);
|
|
63
|
+
if (!poppy)
|
|
64
|
+
return;
|
|
65
|
+
withNonce(ctx, poppy);
|
|
66
|
+
try {
|
|
67
|
+
const body = await poppy.token(form(ctx), ctx.request.header('dpop'));
|
|
68
|
+
noStore(ctx);
|
|
69
|
+
return ctx.response.json(body);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof PoppyError)
|
|
73
|
+
return oauthError(ctx, error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async deviceAuthorization(ctx) {
|
|
78
|
+
const poppy = await poppyOr404(ctx);
|
|
79
|
+
if (!poppy)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
const body = await poppy.deviceAuthorization(form(ctx));
|
|
83
|
+
noStore(ctx);
|
|
84
|
+
return ctx.response.json(body);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof PoppyError)
|
|
88
|
+
return oauthError(ctx, error);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** RFC 7009: 200 também para token desconhecido. */
|
|
93
|
+
async revoke(ctx) {
|
|
94
|
+
const poppy = await poppyOr404(ctx);
|
|
95
|
+
if (!poppy)
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
await poppy.revoke(form(ctx));
|
|
99
|
+
noStore(ctx);
|
|
100
|
+
return ctx.response.status(200).send('');
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof PoppyError)
|
|
104
|
+
return oauthError(ctx, error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** `POST auth.mediated.endpoint` (§4.7) — com o Session Token (DPoP). */
|
|
109
|
+
async mediatedStart(ctx) {
|
|
110
|
+
const poppy = await poppyOr404(ctx);
|
|
111
|
+
if (!poppy?.cfg.signIn.mediated)
|
|
112
|
+
return poppy ? ctx.response.notFound() : undefined;
|
|
113
|
+
const auth = await poppy.verifyAccess({
|
|
114
|
+
method: ctx.request.method(),
|
|
115
|
+
url: publicRequestUrl(ctx, poppy),
|
|
116
|
+
headers: ctx.request.headers(),
|
|
117
|
+
});
|
|
118
|
+
if (!auth.ok) {
|
|
119
|
+
const { ok: _ok, ...rest } = auth;
|
|
120
|
+
return writePoppyAuthError(ctx, { ok: false, ...rest });
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const body = await poppy.mediatedStart(auth.principal, auth.jkt, ctx.request.body(), ctx);
|
|
124
|
+
noStore(ctx);
|
|
125
|
+
return ctx.response.json(body);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error instanceof PoppyError)
|
|
129
|
+
return oauthError(ctx, error);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** `POST {mediated}/{sign_in_id}` (§4.7). */
|
|
134
|
+
async mediatedCode(ctx) {
|
|
135
|
+
const poppy = await poppyOr404(ctx);
|
|
136
|
+
if (!poppy?.cfg.signIn.mediated)
|
|
137
|
+
return poppy ? ctx.response.notFound() : undefined;
|
|
138
|
+
const auth = await poppy.verifyAccess({
|
|
139
|
+
method: ctx.request.method(),
|
|
140
|
+
url: publicRequestUrl(ctx, poppy),
|
|
141
|
+
headers: ctx.request.headers(),
|
|
142
|
+
});
|
|
143
|
+
if (!auth.ok) {
|
|
144
|
+
const { ok: _ok, ...rest } = auth;
|
|
145
|
+
return writePoppyAuthError(ctx, { ok: false, ...rest });
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const body = await poppy.mediatedCode(auth.principal, auth.jkt, String(ctx.request.param('id') ?? ''), ctx.request.body());
|
|
149
|
+
noStore(ctx);
|
|
150
|
+
if (!body) {
|
|
151
|
+
return ctx.response
|
|
152
|
+
.status(404)
|
|
153
|
+
.json({ error: 'invalid_request', error_description: 'Unknown sign_in_id' });
|
|
154
|
+
}
|
|
155
|
+
return ctx.response.json(body);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof PoppyError)
|
|
159
|
+
return oauthError(ctx, error);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* `POST web.browser_session_endpoint` (§5): asserção no corpo → cookie do app
|
|
165
|
+
* para a Session e 303 para `return_to`; qualquer falha → 400, sem cookie.
|
|
166
|
+
*/
|
|
167
|
+
async browserSession(ctx) {
|
|
168
|
+
const poppy = await poppyOr404(ctx);
|
|
169
|
+
if (!poppy?.cfg.web?.browserSession)
|
|
170
|
+
return poppy ? ctx.response.notFound() : undefined;
|
|
171
|
+
noStore(ctx);
|
|
172
|
+
// A asserção vai NO CORPO, nunca na URL (§5).
|
|
173
|
+
if (ctx.request.qs().assertion !== undefined)
|
|
174
|
+
return badBrowserRequest(ctx);
|
|
175
|
+
let result;
|
|
176
|
+
try {
|
|
177
|
+
result = await poppy.verifyBrowserAssertion(ctx.request.input('assertion'), new URL(publicRequestUrl(ctx, poppy)).origin);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof PoppyError)
|
|
181
|
+
return badBrowserRequest(ctx);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
await loginPoppyBrowser(ctx, result);
|
|
185
|
+
ctx.response.header('Location', result.returnTo);
|
|
186
|
+
return ctx.response.status(303).send('');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function badBrowserRequest(ctx) {
|
|
190
|
+
ctx.response.header('Content-Type', 'text/html; charset=utf-8');
|
|
191
|
+
return ctx.response
|
|
192
|
+
.status(400)
|
|
193
|
+
.send('<!doctype html><html><head><meta charset="utf-8"><title>Bad request</title></head><body><h1>Invalid browser session request</h1></body></html>');
|
|
194
|
+
}
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface AuthkitCsrfOptions {
|
|
|
10
10
|
* consentimento (`{prefix}/consent`) continua protegida.
|
|
11
11
|
*/
|
|
12
12
|
personalAgentsPrefix?: string | false;
|
|
13
|
+
/**
|
|
14
|
+
* Prefixo do Poppy: isenta token/revoke/device authorization, o Mediated
|
|
15
|
+
* Sign-In e a sessão de navegador (POST cross-site autenticado pela asserção).
|
|
16
|
+
* Default: o do config. As telas `{prefix}/oauth/authorize` e `{prefix}/device`
|
|
17
|
+
* continuam protegidas. `false` desliga.
|
|
18
|
+
*/
|
|
19
|
+
poppyPrefix?: string | false;
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
package/build/src/host/csrf.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
import { normalizePersonalAgentsPrefix } from '../agents/config.js';
|
|
19
|
+
import { normalizePoppyPrefix } from '../agents/poppy/config.js';
|
|
19
20
|
import { getAuthHostConfig } from './auth_host_config.js';
|
|
20
21
|
/**
|
|
21
22
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
@@ -33,8 +34,22 @@ export function authkitCsrfExceptions(url, options = {}) {
|
|
|
33
34
|
? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
|
|
34
35
|
: getAuthHostConfig()?.personalAgents?.prefix;
|
|
35
36
|
const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
|
|
37
|
+
const poppyPrefix = options.poppyPrefix === false
|
|
38
|
+
? undefined
|
|
39
|
+
: options.poppyPrefix !== undefined
|
|
40
|
+
? normalizePoppyPrefix(options.poppyPrefix)
|
|
41
|
+
: getAuthHostConfig()?.personalAgents?.poppyPrefix;
|
|
42
|
+
const path = url.split('?')[0];
|
|
43
|
+
const poppyExempt = !!poppyPrefix &&
|
|
44
|
+
(path === `${poppyPrefix}/oauth/token` ||
|
|
45
|
+
path === `${poppyPrefix}/oauth/revoke` ||
|
|
46
|
+
path === `${poppyPrefix}/oauth/device` ||
|
|
47
|
+
path === `${poppyPrefix}/sign-in` ||
|
|
48
|
+
path.startsWith(`${poppyPrefix}/sign-in/`) ||
|
|
49
|
+
path === `${poppyPrefix}/browser-session`);
|
|
36
50
|
return (url.includes(mountPath) ||
|
|
37
51
|
url.includes('/authkit/pat') ||
|
|
38
52
|
(backchannel !== null && url === backchannel) ||
|
|
39
|
-
(agentsOAuth !== null && url.startsWith(agentsOAuth))
|
|
53
|
+
(agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
|
|
54
|
+
poppyExempt);
|
|
40
55
|
}
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -204,6 +204,8 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
204
204
|
'agents.done.revoke_hint': string;
|
|
205
205
|
'agents.done.expired_title': string;
|
|
206
206
|
'agents.done.expired_body': string;
|
|
207
|
+
'agents.done.invalid_title': string;
|
|
208
|
+
'agents.done.invalid_body': string;
|
|
207
209
|
'account.apps.not_supported': string;
|
|
208
210
|
'account.orgs.page_title': string;
|
|
209
211
|
'account.orgs.title': string;
|
|
@@ -956,6 +958,8 @@ export declare const PT_BR_MESSAGES: {
|
|
|
956
958
|
'agents.done.revoke_hint': string;
|
|
957
959
|
'agents.done.expired_title': string;
|
|
958
960
|
'agents.done.expired_body': string;
|
|
961
|
+
'agents.done.invalid_title': string;
|
|
962
|
+
'agents.done.invalid_body': string;
|
|
959
963
|
'account.apps.not_supported': string;
|
|
960
964
|
'account.orgs.page_title': string;
|
|
961
965
|
'account.orgs.title': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -212,6 +212,8 @@ export const DEFAULT_MESSAGES = {
|
|
|
212
212
|
'agents.done.revoke_hint': 'To revoke this access later, go to the apps page of your account.',
|
|
213
213
|
'agents.done.expired_title': 'Request expired',
|
|
214
214
|
'agents.done.expired_body': 'This request is no longer valid. Ask your assistant to start again.',
|
|
215
|
+
'agents.done.invalid_title': 'Invalid request',
|
|
216
|
+
'agents.done.invalid_body': 'This sign-in link is not valid. Go back to your assistant and start again.',
|
|
215
217
|
'account.apps.not_supported': 'The configured OIDC adapter does not support enumeration — listing apps is unavailable.',
|
|
216
218
|
// Console de conta — organizations (account/orgs).
|
|
217
219
|
'account.orgs.page_title': 'My Organizations',
|
|
@@ -1052,6 +1054,8 @@ export const PT_BR_MESSAGES = {
|
|
|
1052
1054
|
'agents.done.revoke_hint': 'Para revogar este acesso depois, vá à página de apps da sua conta.',
|
|
1053
1055
|
'agents.done.expired_title': 'Pedido expirado',
|
|
1054
1056
|
'agents.done.expired_body': 'Este pedido não vale mais. Peça ao seu assistente para começar de novo.',
|
|
1057
|
+
'agents.done.invalid_title': 'Pedido inválido',
|
|
1058
|
+
'agents.done.invalid_body': 'Este link de acesso não é válido. Volte ao seu assistente e comece de novo.',
|
|
1055
1059
|
'account.apps.not_supported': 'O adapter OIDC configurado não suporta enumeração — a listagem de apps fica indisponível.',
|
|
1056
1060
|
// Console de conta — organizations (account/orgs).
|
|
1057
1061
|
'account.orgs.page_title': 'Minhas Organizações',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session key que marca a sessão do navegador de um personal agent Poppy (§5):
|
|
3
|
+
* `{ sessionId, clientId, userId }`. Import-free (o barrel a reexporta — ver
|
|
4
|
+
* `account_session_key.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Uma sessão marcada é do AGENTE, não do usuário: o console de conta, o admin e
|
|
7
|
+
* as telas de consentimento a recusam.
|
|
8
|
+
*/
|
|
9
|
+
export declare const POPPY_WEB_SESSION_KEY = "authkit_poppy_session";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session key que marca a sessão do navegador de um personal agent Poppy (§5):
|
|
3
|
+
* `{ sessionId, clientId, userId }`. Import-free (o barrel a reexporta — ver
|
|
4
|
+
* `account_session_key.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Uma sessão marcada é do AGENTE, não do usuário: o console de conta, o admin e
|
|
7
|
+
* as telas de consentimento a recusam.
|
|
8
|
+
*/
|
|
9
|
+
export const POPPY_WEB_SESSION_KEY = 'authkit_poppy_session';
|
|
@@ -8,6 +8,7 @@ import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
|
8
8
|
import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
|
|
9
9
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
10
10
|
import { ensureConsoleSession } from './idp_session_bridge.js';
|
|
11
|
+
import { POPPY_WEB_SESSION_KEY } from './poppy_session_key.js';
|
|
11
12
|
import { createAuthThrottles } from './rate_limit.js';
|
|
12
13
|
import { redirectExact } from './redirect_exact.js';
|
|
13
14
|
import { requestPathWithQuery } from './request_url.js';
|
|
@@ -113,7 +114,16 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
113
114
|
* classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
|
|
114
115
|
* /account/mfa acessíveis sem sessão.
|
|
115
116
|
*/
|
|
117
|
+
/** A sessão é do navegador de um personal agent Poppy (§5)? */
|
|
118
|
+
function isPoppyBrowser(ctx) {
|
|
119
|
+
const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
|
|
120
|
+
return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
|
|
121
|
+
}
|
|
116
122
|
const accountGuard = async (ctx, next) => {
|
|
123
|
+
// O navegador de um personal agent Poppy (§5) não entra no console de conta nem
|
|
124
|
+
// nas telas de consentimento: a sessão é do agente, não do usuário.
|
|
125
|
+
if (isPoppyBrowser(ctx))
|
|
126
|
+
return ctx.response.status(403).send('');
|
|
117
127
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
118
128
|
if (!(await ensureConsoleSession(ctx))) {
|
|
119
129
|
return redirectExact(ctx.response, buildLoginRedirect(ctx));
|
|
@@ -145,6 +155,8 @@ export const adminGuard = async (ctx, next) => {
|
|
|
145
155
|
if (!cfg.admin.enabled) {
|
|
146
156
|
return ctx.response.notFound();
|
|
147
157
|
}
|
|
158
|
+
if (isPoppyBrowser(ctx))
|
|
159
|
+
return ctx.response.status(403).send('');
|
|
148
160
|
// Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
|
|
149
161
|
await ensureConsoleSession(ctx);
|
|
150
162
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
@@ -187,6 +199,8 @@ const C = {
|
|
|
187
199
|
accountConfirm: () => import('./controllers/account_confirm_controller.js'),
|
|
188
200
|
agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
|
|
189
201
|
agentConsent: () => import('./controllers/agent_consent_controller.js'),
|
|
202
|
+
poppy: () => import('./controllers/poppy_controller.js'),
|
|
203
|
+
poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
|
|
190
204
|
webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
|
|
191
205
|
logoutAsset: () => import('./controllers/logout_asset_controller.js'),
|
|
192
206
|
passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
|
|
@@ -206,6 +220,7 @@ const C = {
|
|
|
206
220
|
consoleSettings: () => import('./admin_console/console_settings_controller.js'),
|
|
207
221
|
consoleKeys: () => import('./admin_console/console_keys_controller.js'),
|
|
208
222
|
consoleImpersonation: () => import('./admin_console/console_impersonation_controller.js'),
|
|
223
|
+
consolePersonalAgents: () => import('./admin_console/console_personal_agents_controller.js'),
|
|
209
224
|
apiUsers: () => import('./admin_api/api_users_controller.js'),
|
|
210
225
|
apiClients: () => import('./admin_api/api_clients_controller.js'),
|
|
211
226
|
apiMisc: () => import('./admin_api/api_misc_controller.js'),
|
|
@@ -508,6 +523,52 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
508
523
|
})
|
|
509
524
|
.use([accountGuard]);
|
|
510
525
|
}
|
|
526
|
+
// Personal Agent Protocol (Poppy): descoberta, authorization server (token,
|
|
527
|
+
// revogação, device authorization), Mediated Sign-In e sessão de navegador —
|
|
528
|
+
// server-to-server ou POST cross-site do navegador do agente (sem CSRF, ver
|
|
529
|
+
// `authkitCsrfExceptions`). As telas do usuário (authorize/device) exigem a
|
|
530
|
+
// sessão de conta e mantêm o CSRF.
|
|
531
|
+
const poppyPrefix = hostCfg?.personalAgents?.poppyPrefix;
|
|
532
|
+
if (poppyPrefix) {
|
|
533
|
+
// Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
|
|
534
|
+
// (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
|
|
535
|
+
router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
|
|
536
|
+
router
|
|
537
|
+
.get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
|
|
538
|
+
.as('authkit.poppy.metadata');
|
|
539
|
+
router
|
|
540
|
+
.get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
|
|
541
|
+
.as('authkit.poppy.metadata_under_issuer');
|
|
542
|
+
router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
|
|
543
|
+
router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
|
|
544
|
+
router
|
|
545
|
+
.post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
|
|
546
|
+
.as('authkit.poppy.device_authorization');
|
|
547
|
+
// Credenciais e códigos de uso único: throttle por IP (§4.7 exige rate limit).
|
|
548
|
+
withLogin(router.post(`${poppyPrefix}/sign-in`, [C.poppy, 'mediatedStart']).as('authkit.poppy.sign_in'));
|
|
549
|
+
withOtpLogin(router
|
|
550
|
+
.post(`${poppyPrefix}/sign-in/:id`, [C.poppy, 'mediatedCode'])
|
|
551
|
+
.as('authkit.poppy.sign_in_code'));
|
|
552
|
+
router
|
|
553
|
+
.post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
|
|
554
|
+
.as('authkit.poppy.browser_session');
|
|
555
|
+
router
|
|
556
|
+
.group(() => {
|
|
557
|
+
router
|
|
558
|
+
.get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
|
|
559
|
+
.as('authkit.poppy.authorize');
|
|
560
|
+
router
|
|
561
|
+
.post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
|
|
562
|
+
.as('authkit.poppy.authorize_decide');
|
|
563
|
+
withOtpLogin(router
|
|
564
|
+
.get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
|
|
565
|
+
.as('authkit.poppy.device'));
|
|
566
|
+
withOtpLogin(router
|
|
567
|
+
.post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
|
|
568
|
+
.as('authkit.poppy.device_decide'));
|
|
569
|
+
})
|
|
570
|
+
.use([accountGuard]);
|
|
571
|
+
}
|
|
511
572
|
// Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
|
|
512
573
|
// As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
|
|
513
574
|
// action-subpaths concatenados (`/password`, `/enroll`, ...) são FIXOS —
|
|
@@ -855,6 +916,21 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
855
916
|
// Chave de assinatura managed (status + rotação ao vivo).
|
|
856
917
|
router.get(`${ap}/api/keys`, [C.consoleKeys, 'status']);
|
|
857
918
|
router.post(`${ap}/api/keys/rotate`, [C.consoleKeys, 'rotate']);
|
|
919
|
+
// Personal agents (capability-gated: 404 sem `personalAgents` ou sem banco).
|
|
920
|
+
router.get(`${ap}/api/personal-agents`, [C.consolePersonalAgents, 'index']);
|
|
921
|
+
router.get(`${ap}/api/personal-agents/connections`, [
|
|
922
|
+
C.consolePersonalAgents,
|
|
923
|
+
'connections',
|
|
924
|
+
]);
|
|
925
|
+
router.post(`${ap}/api/personal-agents/connections/:grantId/revoke`, [
|
|
926
|
+
C.consolePersonalAgents,
|
|
927
|
+
'revokeConnection',
|
|
928
|
+
]);
|
|
929
|
+
router.post(`${ap}/api/personal-agents/registry`, [C.consolePersonalAgents, 'upsert']);
|
|
930
|
+
router.delete(`${ap}/api/personal-agents/registry/:id`, [
|
|
931
|
+
C.consolePersonalAgents,
|
|
932
|
+
'destroy',
|
|
933
|
+
]);
|
|
858
934
|
// Impersonation (capability-gated: 404 quando desabilitado ou sem client).
|
|
859
935
|
router.get(`${ap}/api/impersonation/:userId`, [C.consoleImpersonation, 'handle']);
|
|
860
936
|
// ─── Shell HTML — POR ÚLTIMO (catch-all) ──────────────────────────────
|