@adonis-agora/authkit-server 1.0.0-rc.0 → 1.0.0-rc.2
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/CHANGELOG.md +38 -0
- package/README.md +4 -5
- package/build/index.d.ts +7 -11
- package/build/index.js +5 -9
- package/build/providers/authkit_server_provider.js +14 -8
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.js +1 -1
- package/build/src/console_authorize.d.ts +56 -0
- package/build/src/console_authorize.js +85 -0
- package/build/src/define_config.d.ts +20 -14
- package/build/src/define_config.js +11 -8
- package/build/src/doctor/checks.d.ts +7 -1
- package/build/src/doctor/checks.js +47 -1
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_clients_service.js +10 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console_warning.d.ts +8 -0
- package/build/src/host/admin_console_warning.js +18 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +11 -2
- package/build/src/host/console_denied_page.d.ts +6 -0
- package/build/src/host/console_denied_page.js +18 -0
- package/build/src/host/console_sso.d.ts +24 -0
- package/build/src/host/console_sso.js +70 -0
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/controllers/account_session_controller.js +10 -1
- package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
- package/build/src/host/controllers/console_sso_controller.js +61 -0
- package/build/src/host/controllers/interaction_controller.js +3 -1
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +3 -14
- package/build/src/host/i18n.js +15 -13
- package/build/src/host/register_auth_host.d.ts +2 -0
- package/build/src/host/register_auth_host.js +110 -64
- package/build/src/host/ui-dist/assets/index-BqyJYRVX.js +67 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/build_provider.js +6 -0
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +14 -0
- package/build/src/schema/ensure.js +43 -150
- package/package.json +5 -5
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
|
@@ -611,7 +611,7 @@ export default class AccountApiController {
|
|
|
611
611
|
/** Personal agents com delegação ativa na conta (scopes aprovados). */
|
|
612
612
|
async listAgents(ctx) {
|
|
613
613
|
const runtime = await personalAgentsFor(ctx);
|
|
614
|
-
if (!runtime
|
|
614
|
+
if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
|
|
615
615
|
return ctx.response
|
|
616
616
|
.status(404)
|
|
617
617
|
.send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
|
|
@@ -620,16 +620,18 @@ export default class AccountApiController {
|
|
|
620
620
|
const pact = runtime.delegation
|
|
621
621
|
? (await runtime.delegation.listGrants(userId)).map((g) => ({ ...g, protocol: 'pact' }))
|
|
622
622
|
: [];
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
623
|
+
const others = [];
|
|
624
|
+
for (const [protocol, { runtime: extension }] of runtime.extensions) {
|
|
625
|
+
for (const g of await extension.listAccountGrants(userId))
|
|
626
|
+
others.push({ ...g, protocol });
|
|
627
|
+
}
|
|
628
|
+
return { agents: [...pact, ...others] };
|
|
627
629
|
}
|
|
628
630
|
// ─── DELETE /account/api/agents/:id ──────────────────────────────────────
|
|
629
631
|
/** Revoga a delegação de um personal agent (efeito imediato nos tokens). */
|
|
630
632
|
async revokeAgent(ctx) {
|
|
631
633
|
const runtime = await personalAgentsFor(ctx);
|
|
632
|
-
if (!runtime
|
|
634
|
+
if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
|
|
633
635
|
return ctx.response
|
|
634
636
|
.status(404)
|
|
635
637
|
.send(apiErr('capability_unsupported', 'Personal agent delegation is not enabled.'));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abrir e fechar a sessão de CONTA do authkit (o cookie do console, `ACCOUNT_SESSION_KEY`) a partir
|
|
3
|
+
* de código de fora do host-kit — o navegador de um personal agent que entrou na conta, por
|
|
4
|
+
* exemplo. Sincroniza `ctx.auth` (`adonisAuth.guard`) como o login do próprio console.
|
|
5
|
+
*
|
|
6
|
+
* Estável no 1.x (usado pelas `personalAgents.extensions`).
|
|
7
|
+
*/
|
|
8
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
9
|
+
/** Liga a sessão de conta a `accountId` e sincroniza `ctx.auth`. Não regenera a sessão. */
|
|
10
|
+
export declare function signInAccountSession(ctx: HttpContext, accountId: string): Promise<void>;
|
|
11
|
+
/** Desliga a sessão de conta e sincroniza `ctx.auth`. Não regenera a sessão. */
|
|
12
|
+
export declare function signOutAccountSession(ctx: HttpContext): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
2
|
+
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from './adonis_auth_sync.js';
|
|
3
|
+
/** Liga a sessão de conta a `accountId` e sincroniza `ctx.auth`. Não regenera a sessão. */
|
|
4
|
+
export async function signInAccountSession(ctx, accountId) {
|
|
5
|
+
// Sem augmentation no barrel (ver `augmentation_isolation.spec.ts`): a sessão é lida sem tipo.
|
|
6
|
+
const session = ctx.session;
|
|
7
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
8
|
+
session.put(ACCOUNT_SESSION_KEY, accountId);
|
|
9
|
+
const account = await service.config.accountStore.findById(accountId);
|
|
10
|
+
if (account)
|
|
11
|
+
await syncAdonisAuthLogin(ctx, service.config, account);
|
|
12
|
+
}
|
|
13
|
+
/** Desliga a sessão de conta e sincroniza `ctx.auth`. Não regenera a sessão. */
|
|
14
|
+
export async function signOutAccountSession(ctx) {
|
|
15
|
+
const session = ctx.session;
|
|
16
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
17
|
+
session.forget(ACCOUNT_SESSION_KEY);
|
|
18
|
+
await syncAdonisAuthLogout(ctx, service.config);
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { assertClientMetadata } from './client_metadata.js';
|
|
2
|
+
import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
|
|
3
|
+
import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
|
|
3
4
|
/** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
|
|
4
5
|
const PUBLIC_AUTH_METHODS = new Set(['none']);
|
|
5
6
|
/** Gera um identificador opaco no estilo do oidc-provider (~43 chars base64url). */
|
|
@@ -51,6 +52,7 @@ export class AdminClientsService {
|
|
|
51
52
|
*/
|
|
52
53
|
async create(input) {
|
|
53
54
|
const clientId = (input.clientId ?? '').trim() || randomId();
|
|
55
|
+
assertNotReservedClientId(clientId);
|
|
54
56
|
const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
|
|
55
57
|
const clientSecret = confidential ? randomId() : undefined;
|
|
56
58
|
const payload = this.#buildPayload(clientId, input, clientSecret);
|
|
@@ -68,6 +70,7 @@ export class AdminClientsService {
|
|
|
68
70
|
*/
|
|
69
71
|
async createWithSecret(input, providedSecret) {
|
|
70
72
|
const clientId = (input.clientId ?? '').trim() || randomId();
|
|
73
|
+
assertNotReservedClientId(clientId);
|
|
71
74
|
const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
|
|
72
75
|
const clientSecret = confidential ? (providedSecret ?? randomId()) : undefined;
|
|
73
76
|
const payload = this.#buildPayload(clientId, input, clientSecret);
|
|
@@ -215,3 +218,9 @@ export class AdminClientsService {
|
|
|
215
218
|
return result;
|
|
216
219
|
}
|
|
217
220
|
}
|
|
221
|
+
/** O `client_id` do client interno do console (ver `console_sso.ts`) não pode ser cadastrado. */
|
|
222
|
+
function assertNotReservedClientId(clientId) {
|
|
223
|
+
if (clientId === CONSOLE_SSO_CLIENT_ID) {
|
|
224
|
+
throw new ClientMetadataError(`client_id "${clientId}" é reservado pelo authkit`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -22,7 +22,7 @@ interface AgentEntry {
|
|
|
22
22
|
connections: number;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Endpoints JSON de personal agents (PACT e
|
|
25
|
+
* Endpoints JSON de personal agents (PACT e `personalAgents.extensions`) do console admin React.
|
|
26
26
|
*
|
|
27
27
|
* GET {prefix}/api/personal-agents → agentes (config + console + conectados)
|
|
28
28
|
* GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
|
|
@@ -41,11 +41,6 @@ export default class ConsolePersonalAgentsController {
|
|
|
41
41
|
/** A lista do config é uma função do host (o console não enxerga). */
|
|
42
42
|
hostResolver: boolean;
|
|
43
43
|
} | null;
|
|
44
|
-
poppy: {
|
|
45
|
-
requireRegistration: boolean;
|
|
46
|
-
hostResolver: boolean;
|
|
47
|
-
allowInsecureClientIds: boolean;
|
|
48
|
-
} | null;
|
|
49
44
|
};
|
|
50
45
|
agents: AgentEntry[];
|
|
51
46
|
}>;
|
|
@@ -31,10 +31,21 @@ function enabledProtocols(runtime) {
|
|
|
31
31
|
const out = [];
|
|
32
32
|
if (runtime.config.pact)
|
|
33
33
|
out.push('pact');
|
|
34
|
-
|
|
35
|
-
out.push('poppy');
|
|
34
|
+
out.push(...runtime.extensions.keys());
|
|
36
35
|
return out;
|
|
37
36
|
}
|
|
37
|
+
/** O protocolo dono de um grant, pelo prefixo do id. */
|
|
38
|
+
function protocolOfGrant(runtime, grantId) {
|
|
39
|
+
for (const [id, { extension }] of runtime.extensions) {
|
|
40
|
+
if (grantId.startsWith(extension.grantIdPrefix))
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
return 'pact';
|
|
44
|
+
}
|
|
45
|
+
/** Prefixo de id dos grants de um protocolo (`agrant_` = PACT). */
|
|
46
|
+
function grantPrefixOf(runtime, protocol) {
|
|
47
|
+
return runtime.extensions.get(protocol)?.extension.grantIdPrefix ?? 'agrant_';
|
|
48
|
+
}
|
|
38
49
|
function isUrl(value, allowHttp) {
|
|
39
50
|
if (typeof value !== 'string' || value.length > 2048)
|
|
40
51
|
return false;
|
|
@@ -52,14 +63,14 @@ function isUrl(value, allowHttp) {
|
|
|
52
63
|
async function agentName(runtime, protocol, identifier, fallback) {
|
|
53
64
|
if (fallback)
|
|
54
65
|
return fallback;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
const extension = runtime.extensions.get(protocol);
|
|
67
|
+
if (extension)
|
|
68
|
+
return extension.runtime.displayName(identifier);
|
|
58
69
|
const registration = await runtime.config.resolveAgent(identifier);
|
|
59
70
|
return displayNameFor(registration ?? { issuer: identifier, jwksUri: '' });
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
|
-
* Endpoints JSON de personal agents (PACT e
|
|
73
|
+
* Endpoints JSON de personal agents (PACT e `personalAgents.extensions`) do console admin React.
|
|
63
74
|
*
|
|
64
75
|
* GET {prefix}/api/personal-agents → agentes (config + console + conectados)
|
|
65
76
|
* GET {prefix}/api/personal-agents/connections?clientId=&accountId=&protocol=&page=&size=
|
|
@@ -105,17 +116,20 @@ export default class ConsolePersonalAgentsController {
|
|
|
105
116
|
e.jwksUri = agent.jwksUri;
|
|
106
117
|
names.set(keyOf('pact', agent.issuer), agent.name ?? null);
|
|
107
118
|
}
|
|
108
|
-
for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, '
|
|
119
|
+
for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, 'agrant_')) {
|
|
109
120
|
entry('pact', clientId).connections = count;
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
const summaries = {};
|
|
124
|
+
for (const [protocol, { extension, runtime: ext }] of runtime.extensions) {
|
|
125
|
+
const summary = ext.consoleSummary();
|
|
126
|
+
summaries[protocol] = { label: extension.label, ...summary };
|
|
127
|
+
for (const id of summary.configured.allowed)
|
|
128
|
+
entry(protocol, id).config = 'allowed';
|
|
129
|
+
for (const id of summary.configured.blocked)
|
|
130
|
+
entry(protocol, id).config = 'blocked';
|
|
131
|
+
for (const [clientId, count] of await runtime.grants.countLiveGrantsByClient(now, extension.grantIdPrefix)) {
|
|
132
|
+
entry(protocol, clientId).connections = count;
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
135
|
for (const row of await runtime.registry.list()) {
|
|
@@ -153,13 +167,17 @@ export default class ConsolePersonalAgentsController {
|
|
|
153
167
|
hostResolver: cfg.staticAgents === null,
|
|
154
168
|
}
|
|
155
169
|
: null,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
...Object.fromEntries(Object.entries(summaries).map(([protocol, summary]) => [
|
|
171
|
+
protocol,
|
|
172
|
+
{
|
|
173
|
+
label: summary.label,
|
|
174
|
+
requireRegistration: summary.requireRegistration,
|
|
175
|
+
hostResolver: summary.hostResolver,
|
|
176
|
+
allowHttpIdentifiers: summary.allowHttpIdentifiers,
|
|
177
|
+
identifierLabel: summary.identifierLabel,
|
|
178
|
+
identifierPlaceholder: summary.identifierPlaceholder ?? null,
|
|
179
|
+
},
|
|
180
|
+
])),
|
|
163
181
|
},
|
|
164
182
|
agents,
|
|
165
183
|
};
|
|
@@ -172,11 +190,9 @@ export default class ConsolePersonalAgentsController {
|
|
|
172
190
|
const qs = ctx.request.qs();
|
|
173
191
|
const page = parseListPage(qs.page);
|
|
174
192
|
const size = parseListSize(qs.size);
|
|
175
|
-
const protocol = qs.protocol
|
|
176
|
-
? qs.protocol
|
|
177
|
-
: undefined;
|
|
193
|
+
const protocol = qs.protocol && enabledProtocols(runtime).includes(qs.protocol) ? qs.protocol : undefined;
|
|
178
194
|
const filter = {
|
|
179
|
-
protocol,
|
|
195
|
+
idPrefix: protocol ? grantPrefixOf(runtime, protocol) : undefined,
|
|
180
196
|
clientId: qs.clientId?.trim() || undefined,
|
|
181
197
|
accountId: qs.accountId?.trim() || undefined,
|
|
182
198
|
};
|
|
@@ -189,7 +205,7 @@ export default class ConsolePersonalAgentsController {
|
|
|
189
205
|
const emails = new Map();
|
|
190
206
|
const nameCache = new Map();
|
|
191
207
|
const data = await Promise.all(rows.map(async (g) => {
|
|
192
|
-
const rowProtocol = g.id
|
|
208
|
+
const rowProtocol = protocolOfGrant(runtime, g.id);
|
|
193
209
|
if (!emails.has(g.accountId)) {
|
|
194
210
|
const account = await store.findById(g.accountId).catch(() => null);
|
|
195
211
|
emails.set(g.accountId, account?.email ?? null);
|
|
@@ -248,17 +264,21 @@ export default class ConsolePersonalAgentsController {
|
|
|
248
264
|
const invalid = (message) => ctx.response.unprocessableEntity(apiError('invalid_request', message));
|
|
249
265
|
const protocol = body.protocol;
|
|
250
266
|
if (!enabledProtocols(runtime).includes(protocol)) {
|
|
251
|
-
return invalid(
|
|
267
|
+
return invalid(`\`protocol\` precisa ser um protocolo ligado (${enabledProtocols(runtime)
|
|
268
|
+
.map((p) => `\`${p}\``)
|
|
269
|
+
.join(', ')}).`);
|
|
252
270
|
}
|
|
253
271
|
const status = body.status;
|
|
254
272
|
if (status !== 'allowed' && status !== 'blocked') {
|
|
255
273
|
return invalid('`status` precisa ser `allowed` ou `blocked`.');
|
|
256
274
|
}
|
|
257
|
-
const
|
|
275
|
+
const extension = runtime.extensions.get(protocol);
|
|
276
|
+
const summary = extension?.runtime.consoleSummary();
|
|
277
|
+
const allowHttp = !!summary?.allowHttpIdentifiers;
|
|
258
278
|
const identifier = typeof body.identifier === 'string' ? body.identifier.trim() : '';
|
|
259
279
|
if (!isUrl(identifier, allowHttp)) {
|
|
260
|
-
return invalid(
|
|
261
|
-
?
|
|
280
|
+
return invalid(summary
|
|
281
|
+
? `\`identifier\` precisa ser uma URL ${allowHttp ? 'http(s)' : 'HTTPS'}: ${summary.identifierLabel}.`
|
|
262
282
|
: '`identifier` precisa ser o `iss` do agente (URL HTTPS).');
|
|
263
283
|
}
|
|
264
284
|
const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim().slice(0, 200) : null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The boot warning for an admin console with no `admin.authorize`. Same wording in every console. */
|
|
2
|
+
export declare const ADMIN_CONSOLE_NO_AUTHORIZE_WARNING = "[@adonis-agora/authkit-server] console has no `authorize` configured: it is open outside production and denies every request in production. Set `authorize` in config/authkit.ts.";
|
|
3
|
+
/** Logs {@link ADMIN_CONSOLE_NO_AUTHORIZE_WARNING} once per process. */
|
|
4
|
+
export declare function warnAdminConsoleUnauthorized(logger: {
|
|
5
|
+
warn: (message: string) => unknown;
|
|
6
|
+
} | null | undefined): boolean;
|
|
7
|
+
/** Test helper: lets the warning fire again. */
|
|
8
|
+
export declare function resetAdminConsoleWarning(): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The boot warning for an admin console with no `admin.authorize`. Same wording in every console. */
|
|
2
|
+
export const ADMIN_CONSOLE_NO_AUTHORIZE_WARNING = '[@adonis-agora/authkit-server] console has no `authorize` configured: it is open outside production and denies every request in production. Set `authorize` in config/authkit.ts.';
|
|
3
|
+
let warned = false;
|
|
4
|
+
/** Logs {@link ADMIN_CONSOLE_NO_AUTHORIZE_WARNING} once per process. */
|
|
5
|
+
export function warnAdminConsoleUnauthorized(logger) {
|
|
6
|
+
if (warned)
|
|
7
|
+
return false;
|
|
8
|
+
warned = true;
|
|
9
|
+
if (logger)
|
|
10
|
+
logger.warn(ADMIN_CONSOLE_NO_AUTHORIZE_WARNING);
|
|
11
|
+
else
|
|
12
|
+
console.warn(ADMIN_CONSOLE_NO_AUTHORIZE_WARNING);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
/** Test helper: lets the warning fire again. */
|
|
16
|
+
export function resetAdminConsoleWarning() {
|
|
17
|
+
warned = false;
|
|
18
|
+
}
|
|
@@ -87,7 +87,7 @@ 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 (delegações do PACT e
|
|
90
|
+
* agente agindo na conta (delegações do PACT e conexões das extensions). Sem
|
|
91
91
|
* `personalAgents` é no-op — e aí nem resolve o banco. Diferente do
|
|
92
92
|
* `recordSubRevocation`, uma falha AQUI propaga: não há outra camada que corte
|
|
93
93
|
* esses tokens.
|
|
@@ -142,11 +142,14 @@ export class AdminSessionsService {
|
|
|
142
142
|
return [];
|
|
143
143
|
return (await adapter.list()).filter((r) => grantIds.has(r.payload.grantId));
|
|
144
144
|
}
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Grants da conta, exceto os espelhos dos grants das `personalAgents.extensions` (os access
|
|
147
|
+
* tokens de MCP que elas emitem pelo oidc-provider usam o id do grant delas) — aparecem e são
|
|
148
|
+
* revogados como personal agent, não como app OIDC.
|
|
149
|
+
*/
|
|
146
150
|
async #accountGrants(accountId) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !r.id.startsWith('pgrant_'));
|
|
151
|
+
const prefixes = (this.#oidc.config.personalAgents?.extensions ?? []).map((e) => e.grantIdPrefix);
|
|
152
|
+
return (await this.#listModelOfAccount('Grant', accountId)).filter((r) => !prefixes.some((prefix) => r.id.startsWith(prefix)));
|
|
150
153
|
}
|
|
151
154
|
/**
|
|
152
155
|
* Conta TODAS as sessões ativas do IdP (todas as contas). 0 quando o adapter não
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ResolvedPersonalAgentExtension } from '../agents/extension.js';
|
|
2
3
|
import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
|
|
3
4
|
import type { PolicyRouteOption } from './config_locks.js';
|
|
4
5
|
import type { AuthHostOptions } from './register_auth_host.js';
|
|
@@ -35,11 +36,19 @@ export interface AuthHostRuntimeConfig {
|
|
|
35
36
|
* Ver `deriveLockedRouteOptions`.
|
|
36
37
|
*/
|
|
37
38
|
lockedRouteOptions?: PolicyRouteOption[];
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Personal agents ligados → prefixo das rotas do PACT (`/agents`) e as
|
|
41
|
+
* `personalAgents.extensions` resolvidas (rotas, isenções de CSRF, navegador do agente).
|
|
42
|
+
*/
|
|
39
43
|
personalAgents?: {
|
|
40
44
|
prefix: string;
|
|
41
|
-
|
|
45
|
+
extensions?: readonly ResolvedPersonalAgentExtension[];
|
|
42
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* `accountSession.acceptIdpSession: false` com `authMethods: { password: false }`:
|
|
49
|
+
* o console só entra por um `accountLoginUrl` do host (`registerAuthHost` recusa sem ele).
|
|
50
|
+
*/
|
|
51
|
+
consoleNeedsLoginUrl?: boolean;
|
|
43
52
|
/**
|
|
44
53
|
* API headless (Clerk-style). Presente quando o host a declarou — o provider o
|
|
45
54
|
* stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a browser gets when `admin.authorize` denies the admin console
|
|
3
|
+
* (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
|
|
4
|
+
* script and no external asset, so any CSP renders it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function consoleDeniedPage(status: 401 | 403): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a browser gets when `admin.authorize` denies the admin console
|
|
3
|
+
* (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
|
|
4
|
+
* script and no external asset, so any CSP renders it.
|
|
5
|
+
*/
|
|
6
|
+
export function consoleDeniedPage(status) {
|
|
7
|
+
const title = status === 401 ? 'Sign in required' : 'Access denied';
|
|
8
|
+
const body = status === 401
|
|
9
|
+
? 'This console needs credentials this request did not carry.'
|
|
10
|
+
: 'You do not have access to this console.';
|
|
11
|
+
return `<!doctype html>
|
|
12
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
13
|
+
<title>${title}</title>
|
|
14
|
+
<style>body{font-family:system-ui,sans-serif;background:#f9fafb;color:#111827;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:16px}
|
|
15
|
+
.box{max-width:420px;text-align:center}h1{font-size:20px;margin:0 0 8px}p{color:#4b5563;font-size:14px;line-height:1.6;margin:0}
|
|
16
|
+
.code{font-size:12px;color:#9ca3af;margin-bottom:8px}</style>
|
|
17
|
+
</head><body><main class="box"><p class="code">${status}</p><h1>${title}</h1><p>${body}</p></main></body></html>`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
|
|
2
|
+
export declare const CONSOLE_SSO_CLIENT_ID = "authkit-console";
|
|
3
|
+
/** Rota que inicia o login do console pelo IdP. */
|
|
4
|
+
export declare const CONSOLE_SSO_START_PATH = "/auth/console/sso";
|
|
5
|
+
/** `redirect_uri` do client interno: valida o `state` e abre o console. */
|
|
6
|
+
export declare const CONSOLE_SSO_CALLBACK_PATH = "/auth/console/callback";
|
|
7
|
+
/** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
|
|
8
|
+
export declare const CONSOLE_SSO_STATE_KEY = "authkit_console_sso";
|
|
9
|
+
/** O que fica na sessão entre o início e o callback. */
|
|
10
|
+
export interface ConsoleSsoState {
|
|
11
|
+
state: string;
|
|
12
|
+
returnTo: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** Metadata do client interno para o `clients` estático do oidc-provider. */
|
|
15
|
+
export declare function consoleSsoClientMetadata(issuer: string): Record<string, unknown>;
|
|
16
|
+
/** Novo `state` opaco para um login do console. */
|
|
17
|
+
export declare function newConsoleSsoState(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Path do authorize do provider. O issuer termina no `mountPath` (garantido por
|
|
20
|
+
* `registerAuthHost`), então o mount é o path do issuer.
|
|
21
|
+
*/
|
|
22
|
+
export declare function authorizationPath(issuer: string, provider?: any): string;
|
|
23
|
+
/** URL do login do console pelo IdP, preservando o destino como `return_to`. */
|
|
24
|
+
export declare function consoleSsoUrl(returnTo?: string | null): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login do console pelo IdP (SSO), o jeito padrão de entrar em `/account/*` e no
|
|
3
|
+
* console admin.
|
|
4
|
+
*
|
|
5
|
+
* O console é tratado como mais um app do IdP, como o Keycloak faz com o
|
|
6
|
+
* `account-console` e o `security-admin-console`: sem sessão, o visitante vai
|
|
7
|
+
* para a tela de login do IdP (a mesma interaction de qualquer client: senha,
|
|
8
|
+
* magic link, OTP, passkey, social), volta, e a ponte de
|
|
9
|
+
* `idp_session_bridge.ts` abre o console para a conta da sessão do IdP.
|
|
10
|
+
*
|
|
11
|
+
* Para isso existe um client interno, `authkit-console`, injetado como client
|
|
12
|
+
* ESTÁTICO do provider (não fica no adapter, então não aparece na lista de
|
|
13
|
+
* clients do console nem pode ser editado):
|
|
14
|
+
*
|
|
15
|
+
* - `response_types: ['none']`: o authorize não emite code nem token. O client
|
|
16
|
+
* só usa o IdP para autenticar; o que abre o console é a sessão do IdP que o
|
|
17
|
+
* login criou, lida pela ponte. Não há segredo, PKCE ou troca de code.
|
|
18
|
+
* - o consent é concedido automaticamente (é a própria lib).
|
|
19
|
+
* - a única `redirect_uri` é `/auth/console/callback`, no MESMO host do issuer.
|
|
20
|
+
*
|
|
21
|
+
* Só existe com `accountSession.acceptIdpSession` ligado (o default).
|
|
22
|
+
*/
|
|
23
|
+
import { randomBytes } from 'node:crypto';
|
|
24
|
+
/** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
|
|
25
|
+
export const CONSOLE_SSO_CLIENT_ID = 'authkit-console';
|
|
26
|
+
/** Rota que inicia o login do console pelo IdP. */
|
|
27
|
+
export const CONSOLE_SSO_START_PATH = '/auth/console/sso';
|
|
28
|
+
/** `redirect_uri` do client interno: valida o `state` e abre o console. */
|
|
29
|
+
export const CONSOLE_SSO_CALLBACK_PATH = '/auth/console/callback';
|
|
30
|
+
/** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
|
|
31
|
+
export const CONSOLE_SSO_STATE_KEY = 'authkit_console_sso';
|
|
32
|
+
/** Metadata do client interno para o `clients` estático do oidc-provider. */
|
|
33
|
+
export function consoleSsoClientMetadata(issuer) {
|
|
34
|
+
const origin = new URL(issuer).origin;
|
|
35
|
+
return {
|
|
36
|
+
client_id: CONSOLE_SSO_CLIENT_ID,
|
|
37
|
+
client_name: 'Account console',
|
|
38
|
+
application_type: 'web',
|
|
39
|
+
token_endpoint_auth_method: 'none',
|
|
40
|
+
response_types: ['none'],
|
|
41
|
+
grant_types: [],
|
|
42
|
+
redirect_uris: [`${origin}${CONSOLE_SSO_CALLBACK_PATH}`],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Novo `state` opaco para um login do console. */
|
|
46
|
+
export function newConsoleSsoState() {
|
|
47
|
+
return randomBytes(24).toString('base64url');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Path do authorize do provider. O issuer termina no `mountPath` (garantido por
|
|
51
|
+
* `registerAuthHost`), então o mount é o path do issuer.
|
|
52
|
+
*/
|
|
53
|
+
export function authorizationPath(issuer, provider) {
|
|
54
|
+
const mount = new URL(issuer).pathname.replace(/\/+$/, '');
|
|
55
|
+
if (provider && typeof provider.pathFor === 'function') {
|
|
56
|
+
try {
|
|
57
|
+
return provider.pathFor('authorization', { mountPath: mount });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// cai no default do oidc-provider abaixo
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return `${mount}/auth`;
|
|
64
|
+
}
|
|
65
|
+
/** URL do login do console pelo IdP, preservando o destino como `return_to`. */
|
|
66
|
+
export function consoleSsoUrl(returnTo) {
|
|
67
|
+
if (!returnTo)
|
|
68
|
+
return CONSOLE_SSO_START_PATH;
|
|
69
|
+
return `${CONSOLE_SSO_START_PATH}?return_to=${encodeURIComponent(returnTo)}`;
|
|
70
|
+
}
|
|
@@ -38,16 +38,19 @@ export default class AccountAppsController {
|
|
|
38
38
|
scopes: g.scopes,
|
|
39
39
|
})))
|
|
40
40
|
: null;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Conexões das `personalAgents.extensions`: desconectadas pela mesma lista.
|
|
42
|
+
let extensionAgents = null;
|
|
43
|
+
for (const { runtime } of agentsRuntime?.extensions.values() ?? []) {
|
|
44
|
+
extensionAgents ??= [];
|
|
45
|
+
for (const g of await runtime.listAccountGrants(accountId)) {
|
|
46
|
+
extensionAgents.push({
|
|
47
|
+
id: g.id,
|
|
48
|
+
name: await runtime.displayName(g.clientId),
|
|
49
|
+
scopes: g.scopes,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const agents = pactAgents || extensionAgents ? [...(pactAgents ?? []), ...(extensionAgents ?? [])] : null;
|
|
51
54
|
return render(ctx, 'account/apps', {
|
|
52
55
|
csrfToken: ctx.request.csrfToken,
|
|
53
56
|
supported,
|
|
@@ -89,8 +92,9 @@ export default class AccountAppsController {
|
|
|
89
92
|
/** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
|
|
90
93
|
async revokeAgent(ctx) {
|
|
91
94
|
const runtime = await personalAgentsFor(ctx);
|
|
92
|
-
if (!runtime
|
|
95
|
+
if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
|
|
93
96
|
return ctx.response.notFound();
|
|
97
|
+
}
|
|
94
98
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
95
99
|
const cfg = service.config;
|
|
96
100
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
@@ -3,6 +3,7 @@ import { accountHome } from '../account_home.js';
|
|
|
3
3
|
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
4
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
5
5
|
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
6
|
+
import { consoleSsoUrl } from '../console_sso.js';
|
|
6
7
|
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
7
8
|
import { translate } from '../i18n.js';
|
|
8
9
|
import { endBridgedIdpSession } from '../idp_session_bridge.js';
|
|
@@ -49,8 +50,16 @@ export default class AccountSessionController {
|
|
|
49
50
|
return ctx.response.redirect(accountHome(cfg));
|
|
50
51
|
}
|
|
51
52
|
// Lê e valida o return_to da query-string — descarta valores inválidos (open-redirect).
|
|
52
|
-
const
|
|
53
|
+
const qs = (ctx.request.qs?.() ?? {});
|
|
54
|
+
const rawReturnTo = qs.return_to ?? ctx.request.input?.('return_to');
|
|
53
55
|
const returnTo = validateReturnTo(rawReturnTo);
|
|
56
|
+
// SSO (default): o console entra pelo login do IdP, a mesma tela de qualquer
|
|
57
|
+
// app. O formulário de senha daqui fica como porta alternativa
|
|
58
|
+
// (`?method=password`), e só quando a senha não foi desligada no config.
|
|
59
|
+
const passwordForm = qs.method === 'password' && cfg.authMethods?.password !== false;
|
|
60
|
+
if (cfg.accountSession?.acceptIdpSession === true && !passwordForm) {
|
|
61
|
+
return redirectExact(ctx.response, consoleSsoUrl(returnTo));
|
|
62
|
+
}
|
|
54
63
|
const props = {
|
|
55
64
|
csrfToken: ctx.request.csrfToken,
|
|
56
65
|
returnTo,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Login do console pelo IdP (ver `console_sso.ts`).
|
|
5
|
+
*
|
|
6
|
+
* GET /auth/console/sso?return_to=… → authorize do client interno
|
|
7
|
+
* GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
|
|
8
|
+
*/
|
|
9
|
+
export default class ConsoleSsoController {
|
|
10
|
+
start(ctx: HttpContext): Promise<unknown>;
|
|
11
|
+
callback(ctx: HttpContext): Promise<unknown>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { accountHome } from '../account_home.js';
|
|
3
|
+
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
|
+
import { authorizationPath, CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_CLIENT_ID, CONSOLE_SSO_STATE_KEY, newConsoleSsoState, } from '../console_sso.js';
|
|
5
|
+
import { ensureConsoleSession } from '../idp_session_bridge.js';
|
|
6
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
7
|
+
import { validateReturnTo } from './account_session_controller.js';
|
|
8
|
+
/**
|
|
9
|
+
* Login do console pelo IdP (ver `console_sso.ts`).
|
|
10
|
+
*
|
|
11
|
+
* GET /auth/console/sso?return_to=… → authorize do client interno
|
|
12
|
+
* GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
|
|
13
|
+
*/
|
|
14
|
+
export default class ConsoleSsoController {
|
|
15
|
+
async start(ctx) {
|
|
16
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
17
|
+
const cfg = service.config;
|
|
18
|
+
const returnTo = validateReturnTo(ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to'));
|
|
19
|
+
// SSO desligado (`acceptIdpSession: false`): o console só tem o login próprio.
|
|
20
|
+
if (cfg.accountSession?.acceptIdpSession !== true) {
|
|
21
|
+
const login = getAccountLoginUrl();
|
|
22
|
+
const sep = login.includes('?') ? '&' : '?';
|
|
23
|
+
return redirectExact(ctx.response, returnTo ? `${login}${sep}return_to=${encodeURIComponent(returnTo)}` : login);
|
|
24
|
+
}
|
|
25
|
+
// Já tem sessão (de console ou do IdP): não precisa passar pelo IdP.
|
|
26
|
+
if (await ensureConsoleSession(ctx)) {
|
|
27
|
+
return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
|
|
28
|
+
}
|
|
29
|
+
const state = newConsoleSsoState();
|
|
30
|
+
ctx.session.put(CONSOLE_SSO_STATE_KEY, { state, returnTo });
|
|
31
|
+
const origin = new URL(cfg.issuer).origin;
|
|
32
|
+
const query = new URLSearchParams({
|
|
33
|
+
client_id: CONSOLE_SSO_CLIENT_ID,
|
|
34
|
+
response_type: 'none',
|
|
35
|
+
scope: 'openid',
|
|
36
|
+
redirect_uri: `${origin}${CONSOLE_SSO_CALLBACK_PATH}`,
|
|
37
|
+
state,
|
|
38
|
+
});
|
|
39
|
+
return redirectExact(ctx.response, `${authorizationPath(cfg.issuer, service.provider)}?${query.toString()}`);
|
|
40
|
+
}
|
|
41
|
+
async callback(ctx) {
|
|
42
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
43
|
+
const cfg = service.config;
|
|
44
|
+
const pending = ctx.session.get(CONSOLE_SSO_STATE_KEY);
|
|
45
|
+
ctx.session.forget(CONSOLE_SSO_STATE_KEY);
|
|
46
|
+
const qs = (ctx.request.qs?.() ?? {});
|
|
47
|
+
// `state` amarra o callback ao navegador que iniciou o login (anti-CSRF de login).
|
|
48
|
+
if (!pending || typeof qs.state !== 'string' || qs.state !== pending.state) {
|
|
49
|
+
return ctx.response.status(400).send('Invalid or expired sign-in request.');
|
|
50
|
+
}
|
|
51
|
+
// O IdP recusou (o usuário cancelou, conta desabilitada, …): sem console.
|
|
52
|
+
// Vai para a raiz, e não de volta ao console, para não entrar em loop.
|
|
53
|
+
if (typeof qs.error === 'string') {
|
|
54
|
+
return redirectExact(ctx.response, '/');
|
|
55
|
+
}
|
|
56
|
+
if (!(await ensureConsoleSession(ctx))) {
|
|
57
|
+
return redirectExact(ctx.response, '/');
|
|
58
|
+
}
|
|
59
|
+
return redirectExact(ctx.response, pending.returnTo ?? accountHome(cfg));
|
|
60
|
+
}
|
|
61
|
+
}
|