@adonis-agora/authkit-server 0.75.0 → 0.77.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 +148 -0
- package/build/host/views/account/apps.edge +30 -0
- package/build/host/views/agents/consent.edge +60 -0
- package/build/host/views/agents/done.edge +24 -0
- package/build/host/views/consent.edge +9 -1
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +18 -1
- package/build/index.js +9 -1
- package/build/providers/authkit_server_provider.js +75 -50
- package/build/src/accounts/account_store.d.ts +2 -0
- package/build/src/accounts/lucid_account_store.js +2 -0
- package/build/src/adapters/adapter_contract.d.ts +2 -0
- package/build/src/adapters/database_adapter.d.ts +1 -0
- package/build/src/adapters/database_adapter.js +26 -0
- package/build/src/adapters/redis_adapter.d.ts +1 -0
- package/build/src/adapters/redis_adapter.js +30 -0
- package/build/src/agents/agent_identity.d.ts +33 -0
- package/build/src/agents/agent_identity.js +156 -0
- package/build/src/agents/config.d.ts +99 -0
- package/build/src/agents/config.js +101 -0
- package/build/src/agents/delegation_service.d.ts +154 -0
- package/build/src/agents/delegation_service.js +394 -0
- package/build/src/agents/delegation_store.d.ts +93 -0
- package/build/src/agents/delegation_store.js +222 -0
- package/build/src/agents/middleware.d.ts +73 -0
- package/build/src/agents/middleware.js +113 -0
- package/build/src/agents/protocol.d.ts +62 -0
- package/build/src/agents/protocol.js +51 -0
- package/build/src/agents/runtime.d.ts +36 -0
- package/build/src/agents/runtime.js +65 -0
- package/build/src/agents/signer.d.ts +34 -0
- package/build/src/agents/signer.js +70 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
- package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
- package/build/src/define_config.d.ts +50 -3
- package/build/src/define_config.js +42 -1
- package/build/src/host/account_api/account_api_controller.d.ts +12 -0
- package/build/src/host/account_api/account_api_controller.js +40 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_sessions_service.d.ts +2 -0
- package/build/src/host/admin_sessions_service.js +18 -0
- package/build/src/host/auth_host_config.d.ts +4 -0
- package/build/src/host/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
- package/build/src/host/controllers/account_apps_controller.js +39 -3
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
- package/build/src/host/controllers/agent_consent_controller.js +138 -0
- package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
- package/build/src/host/controllers/agent_oauth_controller.js +110 -0
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +380 -24
- package/build/src/host/csrf.d.ts +8 -18
- package/build/src/host/csrf.js +28 -2
- package/build/src/host/custom_login.d.ts +36 -0
- package/build/src/host/custom_login.js +76 -0
- package/build/src/host/custom_login_completion.d.ts +4 -0
- package/build/src/host/custom_login_completion.js +33 -0
- package/build/src/host/custom_mfa.d.ts +77 -0
- package/build/src/host/custom_mfa.js +268 -0
- package/build/src/host/i18n.d.ts +58 -0
- package/build/src/host/i18n.js +64 -6
- package/build/src/host/idp_session_bridge.d.ts +5 -3
- package/build/src/host/idp_session_bridge.js +5 -3
- package/build/src/host/oidc_rp_guard.d.ts +5 -1
- package/build/src/host/oidc_rp_guard.js +15 -2
- package/build/src/host/persistent_rp_session.d.ts +8 -0
- package/build/src/host/persistent_rp_session.js +74 -0
- package/build/src/host/redirect_exact.d.ts +12 -0
- package/build/src/host/redirect_exact.js +17 -0
- package/build/src/host/register_auth_host.js +60 -7
- package/build/src/host/request_url.d.ts +10 -0
- package/build/src/host/request_url.js +17 -0
- package/build/src/host/sudo/methods/magic_link.js +2 -1
- package/build/src/host/sudo/runtime.js +3 -2
- package/build/src/host/sudo_mode.js +4 -4
- package/build/src/host/whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_code_sender.js +9 -0
- package/build/src/host/whatsapp_senders/http.d.ts +5 -0
- package/build/src/host/whatsapp_senders/http.js +33 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
- package/build/src/mcp/mcp_oauth.d.ts +85 -0
- package/build/src/mcp/mcp_oauth.js +154 -0
- package/build/src/provider/build_provider.js +15 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +16 -2
- package/build/src/schema/ensure.js +92 -0
- package/build/stubs/ui/react/pages/consent.tsx +17 -1
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- package/stubs/ui/react/pages/consent.tsx +17 -1
- package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
+
import { displayNameFor } from '../../agents/agent_identity.js';
|
|
3
|
+
import { personalAgentsFor } from '../../agents/runtime.js';
|
|
2
4
|
import { accountPath } from '../account_paths.js';
|
|
3
5
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
4
6
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
7
|
+
import { clientDisplayNames } from '../client_names.js';
|
|
5
8
|
/**
|
|
6
9
|
* Self-service de consentimento ("apps com acesso") no console de conta. Lista os
|
|
7
10
|
* Grants da própria conta agrupados por client (resolvendo o nome do client da
|
|
@@ -19,13 +22,26 @@ export default class AccountAppsController {
|
|
|
19
22
|
const sessions = new AdminSessionsService(service);
|
|
20
23
|
const supported = sessions.canList;
|
|
21
24
|
const grantList = supported ? await sessions.listGrants(accountId) : [];
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const nameOf = (clientId) => clientId ?? '';
|
|
25
|
+
// O nome que o client registrou; sem ele, o clientId.
|
|
26
|
+
const names = await clientDisplayNames(service, grantList.map((g) => g.clientId).filter((id) => !!id));
|
|
27
|
+
const nameOf = (clientId) => clientId ? (names.get(clientId) ?? clientId) : '';
|
|
25
28
|
const revoked = ctx.session.flashMessages.get('appRevoked');
|
|
29
|
+
// Personal agents com delegação ativa (só quando a feature está ligada).
|
|
30
|
+
const agentsRuntime = await personalAgentsFor(ctx);
|
|
31
|
+
const agents = agentsRuntime?.delegation
|
|
32
|
+
? await Promise.all((await agentsRuntime.delegation.listGrants(accountId)).map(async (g) => ({
|
|
33
|
+
id: g.id,
|
|
34
|
+
name: displayNameFor((await agentsRuntime.config.resolveAgent(g.clientId)) ?? {
|
|
35
|
+
issuer: g.clientId,
|
|
36
|
+
jwksUri: '',
|
|
37
|
+
}),
|
|
38
|
+
scopes: g.scopes,
|
|
39
|
+
})))
|
|
40
|
+
: null;
|
|
26
41
|
return render(ctx, 'account/apps', {
|
|
27
42
|
csrfToken: ctx.request.csrfToken,
|
|
28
43
|
supported,
|
|
44
|
+
agents,
|
|
29
45
|
revoked: revoked ?? null,
|
|
30
46
|
apps: grantList
|
|
31
47
|
.filter((g) => !!g.clientId)
|
|
@@ -59,4 +75,24 @@ export default class AccountAppsController {
|
|
|
59
75
|
ctx.session.flash('appRevoked', cfg.messages['account.apps.revoked'] ?? 'account.apps.revoked');
|
|
60
76
|
return ctx.response.redirect(accountPath('apps'));
|
|
61
77
|
}
|
|
78
|
+
/** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
|
|
79
|
+
async revokeAgent(ctx) {
|
|
80
|
+
const runtime = await personalAgentsFor(ctx);
|
|
81
|
+
if (!runtime?.delegation)
|
|
82
|
+
return ctx.response.notFound();
|
|
83
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
84
|
+
const cfg = service.config;
|
|
85
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
86
|
+
const grantId = ctx.request.param('grantId');
|
|
87
|
+
if (await runtime.delegation.revokeGrant(accountId, grantId)) {
|
|
88
|
+
await cfg.audit?.record({
|
|
89
|
+
type: 'agent.grant_revoked',
|
|
90
|
+
accountId,
|
|
91
|
+
ip: ctx.request.ip?.() ?? null,
|
|
92
|
+
metadata: { grantId },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
ctx.session.flash('appRevoked', cfg.messages['account.apps.revoked'] ?? 'account.apps.revoked');
|
|
96
|
+
return ctx.response.redirect(accountPath('apps'));
|
|
97
|
+
}
|
|
62
98
|
}
|
|
@@ -11,6 +11,7 @@ import { revokeOrgAccess } from '../org_access_revocation.js';
|
|
|
11
11
|
// ponto de verdade único, para as duas superfícies nunca divergirem.
|
|
12
12
|
import { effectiveOrgPolicy, orgPolicyDefaults } from '../org_policy.js';
|
|
13
13
|
import { authkitOrigin } from '../origin.js';
|
|
14
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
14
15
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
15
16
|
import { isRoleInCatalog, resolveEffectiveOrganizationsPolicy } from '../runtime_toggles.js';
|
|
16
17
|
/**
|
|
@@ -257,7 +258,7 @@ export default class AccountOrgsController {
|
|
|
257
258
|
const loginUrl = getAccountLoginUrl();
|
|
258
259
|
const returnTo = encodeURIComponent(`${accountPath('orgs')}/invitations/${params.token}/accept`);
|
|
259
260
|
const sep = loginUrl.includes('?') ? '&' : '?';
|
|
260
|
-
return response
|
|
261
|
+
return redirectExact(response, `${loginUrl}${sep}returnTo=${returnTo}`);
|
|
261
262
|
}
|
|
262
263
|
const tokenHash = createHash('sha256').update(params.token).digest('hex');
|
|
263
264
|
const invitation = await store.findInvitationByTokenHash(tokenHash);
|
|
@@ -158,15 +158,19 @@ export default class AccountSecurityController {
|
|
|
158
158
|
return ctx.response.redirect(getAccountLoginUrl());
|
|
159
159
|
}
|
|
160
160
|
const { currentPassword, confirmEmail } = await ctx.request.validateUsing(deleteAccountValidator);
|
|
161
|
-
// Confirmação: senha atual correta OU e-mail digitado batendo com o da conta
|
|
162
|
-
//
|
|
161
|
+
// Confirmação: senha atual correta OU e-mail digitado batendo com o da conta.
|
|
162
|
+
// Contas sem e-mail podem confirmar o telefone já verificado pelo host.
|
|
163
163
|
let confirmed = false;
|
|
164
164
|
if (currentPassword) {
|
|
165
165
|
confirmed = !!(await store.verifyCredentials(account.email, currentPassword));
|
|
166
166
|
}
|
|
167
167
|
if (!confirmed && confirmEmail) {
|
|
168
|
-
confirmed =
|
|
169
|
-
normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email)
|
|
168
|
+
confirmed = account.email
|
|
169
|
+
? normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email)
|
|
170
|
+
: !!account.phone &&
|
|
171
|
+
/^[+\d\s().-]+$/.test(confirmEmail) &&
|
|
172
|
+
/\d/.test(confirmEmail) &&
|
|
173
|
+
confirmEmail.replace(/\D/g, '') === account.phone.replace(/\D/g, '');
|
|
170
174
|
}
|
|
171
175
|
if (!confirmed) {
|
|
172
176
|
ctx.session.flash('deleteError', translate(cfg.messages, 'account.delete.invalid_confirmation'));
|
|
@@ -8,6 +8,7 @@ import { translate } from '../i18n.js';
|
|
|
8
8
|
import { endBridgedIdpSession } from '../idp_session_bridge.js';
|
|
9
9
|
import { attemptPasswordLogin } from '../login_attempt.js';
|
|
10
10
|
import { notifyLoginSuccess } from '../login_notify.js';
|
|
11
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
11
12
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
12
13
|
import { markSudo } from '../sudo_mode.js';
|
|
13
14
|
/**
|
|
@@ -110,7 +111,7 @@ export default class AccountSessionController {
|
|
|
110
111
|
await syncAdonisAuthLogin(ctx, cfg, acc);
|
|
111
112
|
await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email: acc.email, ip });
|
|
112
113
|
// Redireciona pro destino original (validado), ou cai no accountHome configurado.
|
|
113
|
-
return ctx.response
|
|
114
|
+
return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
|
|
114
115
|
}
|
|
115
116
|
async logout(ctx) {
|
|
116
117
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Tela de consentimento da delegação (PACT §5.3) — o `verification_uri` do
|
|
5
|
+
* device flow. Montada atrás do `accountGuard`: sem sessão o usuário passa pelo
|
|
6
|
+
* login DESTE app e volta com o `user_code` no `return_to`. O agente nunca vê
|
|
7
|
+
* esse login.
|
|
8
|
+
*
|
|
9
|
+
* Durante uma impersonation a tela recusa: um admin personificando alguém não
|
|
10
|
+
* pode entregar a conta dessa pessoa a um agente.
|
|
11
|
+
*/
|
|
12
|
+
export default class AgentConsentController {
|
|
13
|
+
/** GET {prefix}/consent?user_code=XXXX-XXXX */
|
|
14
|
+
show(ctx: HttpContext): Promise<any>;
|
|
15
|
+
/** POST {prefix}/consent — `decision=allow|deny`, `scope[]` marcados. */
|
|
16
|
+
decide(ctx: HttpContext): Promise<any>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { displayNameFor } from '../../agents/agent_identity.js';
|
|
3
|
+
import { personalAgentsFor } from '../../agents/runtime.js';
|
|
4
|
+
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
5
|
+
import { impersonationState } from '../impersonation_session.js';
|
|
6
|
+
async function agentName(runtime, issuer) {
|
|
7
|
+
const registration = await runtime.config.resolveAgent(issuer);
|
|
8
|
+
return displayNameFor(registration ?? { issuer, jwksUri: '' });
|
|
9
|
+
}
|
|
10
|
+
/** Host do issuer para exibição — um resolver do host pode devolver issuer que não é URL. */
|
|
11
|
+
function originOf(issuer) {
|
|
12
|
+
try {
|
|
13
|
+
return new URL(issuer).host;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return issuer;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Anti-clickjacking: o atacante pega um código na própria sessão de agente e
|
|
21
|
+
* emoldura o link já preenchido — um clique em "Permitir" entregaria a conta.
|
|
22
|
+
* A tela nunca pode ser renderizada num frame, com ou sem o shield do host.
|
|
23
|
+
*/
|
|
24
|
+
function denyFraming(ctx) {
|
|
25
|
+
ctx.response.header('X-Frame-Options', 'DENY');
|
|
26
|
+
ctx.response.header('Content-Security-Policy', "frame-ancestors 'none'");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Tela de consentimento da delegação (PACT §5.3) — o `verification_uri` do
|
|
30
|
+
* device flow. Montada atrás do `accountGuard`: sem sessão o usuário passa pelo
|
|
31
|
+
* login DESTE app e volta com o `user_code` no `return_to`. O agente nunca vê
|
|
32
|
+
* esse login.
|
|
33
|
+
*
|
|
34
|
+
* Durante uma impersonation a tela recusa: um admin personificando alguém não
|
|
35
|
+
* pode entregar a conta dessa pessoa a um agente.
|
|
36
|
+
*/
|
|
37
|
+
export default class AgentConsentController {
|
|
38
|
+
/** GET {prefix}/consent?user_code=XXXX-XXXX */
|
|
39
|
+
async show(ctx) {
|
|
40
|
+
const runtime = await personalAgentsFor(ctx);
|
|
41
|
+
if (!runtime?.delegation)
|
|
42
|
+
return ctx.response.notFound();
|
|
43
|
+
denyFraming(ctx);
|
|
44
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
45
|
+
const render = service.config.render;
|
|
46
|
+
const base = {
|
|
47
|
+
csrfToken: ctx.request.csrfToken,
|
|
48
|
+
action: runtime.urls.consent,
|
|
49
|
+
request: null,
|
|
50
|
+
account: null,
|
|
51
|
+
error: null,
|
|
52
|
+
};
|
|
53
|
+
if (impersonationState(ctx).active) {
|
|
54
|
+
return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.impersonating' });
|
|
55
|
+
}
|
|
56
|
+
const input = ctx.request.input('user_code');
|
|
57
|
+
if (!input)
|
|
58
|
+
return render(ctx, 'agents/consent', base);
|
|
59
|
+
const pending = await runtime.delegation.pendingRequest(input);
|
|
60
|
+
if (!pending) {
|
|
61
|
+
return render(ctx, 'agents/consent', { ...base, error: 'agents.consent.invalid_code' });
|
|
62
|
+
}
|
|
63
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
64
|
+
const account = await service.config.accountStore.findById(accountId);
|
|
65
|
+
return render(ctx, 'agents/consent', {
|
|
66
|
+
...base,
|
|
67
|
+
request: {
|
|
68
|
+
userCode: pending.userCode,
|
|
69
|
+
agentName: await agentName(runtime, pending.clientId),
|
|
70
|
+
agentOrigin: originOf(pending.clientId),
|
|
71
|
+
scopes: pending.scopes,
|
|
72
|
+
},
|
|
73
|
+
account: { email: account?.email ?? null },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** POST {prefix}/consent — `decision=allow|deny`, `scope[]` marcados. */
|
|
77
|
+
async decide(ctx) {
|
|
78
|
+
const runtime = await personalAgentsFor(ctx);
|
|
79
|
+
if (!runtime?.delegation)
|
|
80
|
+
return ctx.response.notFound();
|
|
81
|
+
denyFraming(ctx);
|
|
82
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
83
|
+
const cfg = service.config;
|
|
84
|
+
const render = cfg.render;
|
|
85
|
+
if (impersonationState(ctx).active) {
|
|
86
|
+
return render(ctx, 'agents/consent', {
|
|
87
|
+
csrfToken: ctx.request.csrfToken,
|
|
88
|
+
action: runtime.urls.consent,
|
|
89
|
+
request: null,
|
|
90
|
+
account: null,
|
|
91
|
+
error: 'agents.consent.impersonating',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
95
|
+
const rawCode = ctx.request.input('user_code');
|
|
96
|
+
const userCode = typeof rawCode === 'string' ? rawCode : '';
|
|
97
|
+
const pending = await runtime.delegation.pendingRequest(userCode);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
return render(ctx, 'agents/done', { status: 'expired', agentName: null, scopes: [] });
|
|
100
|
+
}
|
|
101
|
+
const name = await agentName(runtime, pending.clientId);
|
|
102
|
+
const raw = ctx.request.input('scope');
|
|
103
|
+
const chosen = (Array.isArray(raw) ? raw : raw ? [raw] : []).map(String);
|
|
104
|
+
const approved = ctx.request.input('decision') === 'allow'
|
|
105
|
+
? await runtime.delegation.approve({ userCode, accountId, scopes: chosen })
|
|
106
|
+
: null;
|
|
107
|
+
if (!approved) {
|
|
108
|
+
// Negou ou desmarcou tudo. Se nem negar deu, o pedido mudou de estado no
|
|
109
|
+
// meio do caminho (aprovado em outra aba, expirou).
|
|
110
|
+
if (!(await runtime.delegation.deny({ userCode, accountId }))) {
|
|
111
|
+
return render(ctx, 'agents/done', { status: 'expired', agentName: name, scopes: [] });
|
|
112
|
+
}
|
|
113
|
+
await cfg.audit?.record({
|
|
114
|
+
type: 'agent.delegation_denied',
|
|
115
|
+
accountId,
|
|
116
|
+
clientId: pending.clientId,
|
|
117
|
+
ip: ctx.request.ip?.() ?? null,
|
|
118
|
+
metadata: { requested: pending.scopes.map((s) => s.id) },
|
|
119
|
+
});
|
|
120
|
+
return render(ctx, 'agents/done', { status: 'denied', agentName: name, scopes: [] });
|
|
121
|
+
}
|
|
122
|
+
await cfg.audit?.record({
|
|
123
|
+
type: 'agent.delegation_approved',
|
|
124
|
+
accountId,
|
|
125
|
+
clientId: pending.clientId,
|
|
126
|
+
ip: ctx.request.ip?.() ?? null,
|
|
127
|
+
metadata: { grantId: approved.grantId, scopes: approved.scopes },
|
|
128
|
+
});
|
|
129
|
+
return render(ctx, 'agents/done', {
|
|
130
|
+
status: 'approved',
|
|
131
|
+
agentName: name,
|
|
132
|
+
scopes: approved.scopes.map((id) => ({
|
|
133
|
+
id,
|
|
134
|
+
description: runtime.delegation.scopes[id] ?? id,
|
|
135
|
+
})),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* Authorization server de delegação para personal agents (PACT §5.3): metadata
|
|
4
|
+
* RFC 8414, JWKS e os endpoints RFC 8628 que o AGENTE chama. O cliente OAuth é
|
|
5
|
+
* o agente, autenticado pelo JWT dele em `Authorization: Bearer` em toda
|
|
6
|
+
* chamada — `client_id` tem de ser o `iss` desse JWT.
|
|
7
|
+
*/
|
|
8
|
+
export default class AgentOAuthController {
|
|
9
|
+
#private;
|
|
10
|
+
metadata(ctx: HttpContext): Promise<void | {
|
|
11
|
+
issuer: string;
|
|
12
|
+
device_authorization_endpoint: string;
|
|
13
|
+
token_endpoint: string;
|
|
14
|
+
jwks_uri: string;
|
|
15
|
+
scopes_supported: string[];
|
|
16
|
+
grant_types_supported: string[];
|
|
17
|
+
response_types_supported: never[];
|
|
18
|
+
}>;
|
|
19
|
+
/** Chaves que assinam tokens de delegação e recibos — as mesmas do IdP. */
|
|
20
|
+
jwks(ctx: HttpContext): Promise<any>;
|
|
21
|
+
deviceAuthorization(ctx: HttpContext): Promise<void>;
|
|
22
|
+
token(ctx: HttpContext): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AgentOAuthError, DEVICE_CODE_GRANT_TYPE, REFRESH_TOKEN_GRANT_TYPE, } from '../../agents/delegation_service.js';
|
|
2
|
+
import { personalAgentUnauthorized } from '../../agents/middleware.js';
|
|
3
|
+
import { personalAgentsFor } from '../../agents/runtime.js';
|
|
4
|
+
function noStore(ctx) {
|
|
5
|
+
ctx.response.header('Cache-Control', 'no-store');
|
|
6
|
+
ctx.response.header('Pragma', 'no-cache');
|
|
7
|
+
}
|
|
8
|
+
function oauthError(ctx, error) {
|
|
9
|
+
noStore(ctx);
|
|
10
|
+
return ctx.response
|
|
11
|
+
.status(error.status)
|
|
12
|
+
.json({ error: error.code, error_description: error.description });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Authorization server de delegação para personal agents (PACT §5.3): metadata
|
|
16
|
+
* RFC 8414, JWKS e os endpoints RFC 8628 que o AGENTE chama. O cliente OAuth é
|
|
17
|
+
* o agente, autenticado pelo JWT dele em `Authorization: Bearer` em toda
|
|
18
|
+
* chamada — `client_id` tem de ser o `iss` desse JWT.
|
|
19
|
+
*/
|
|
20
|
+
export default class AgentOAuthController {
|
|
21
|
+
async metadata(ctx) {
|
|
22
|
+
const runtime = await personalAgentsFor(ctx);
|
|
23
|
+
if (!runtime?.delegation)
|
|
24
|
+
return ctx.response.notFound();
|
|
25
|
+
ctx.response.header('Access-Control-Allow-Origin', '*');
|
|
26
|
+
return {
|
|
27
|
+
issuer: runtime.urls.issuer,
|
|
28
|
+
device_authorization_endpoint: runtime.urls.deviceAuthorization,
|
|
29
|
+
token_endpoint: runtime.urls.token,
|
|
30
|
+
jwks_uri: runtime.urls.jwks,
|
|
31
|
+
scopes_supported: Object.keys(runtime.delegation.scopes),
|
|
32
|
+
grant_types_supported: [DEVICE_CODE_GRANT_TYPE, REFRESH_TOKEN_GRANT_TYPE],
|
|
33
|
+
response_types_supported: [],
|
|
34
|
+
// Sem `token_endpoint_auth_methods_supported`: o agente se autentica com o
|
|
35
|
+
// JWT dele em `Authorization: Bearer` (PACT §5.3), método sem nome
|
|
36
|
+
// registrado — anunciar `private_key_jwt` levaria um cliente genérico a
|
|
37
|
+
// mandar `client_assertion` e tomar 401.
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Chaves que assinam tokens de delegação e recibos — as mesmas do IdP. */
|
|
41
|
+
async jwks(ctx) {
|
|
42
|
+
const runtime = await personalAgentsFor(ctx);
|
|
43
|
+
if (!runtime?.delegation)
|
|
44
|
+
return ctx.response.notFound();
|
|
45
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
46
|
+
ctx.response.header('Access-Control-Allow-Origin', '*');
|
|
47
|
+
ctx.response.header('Cache-Control', 'public, max-age=300');
|
|
48
|
+
return service.publicJwks;
|
|
49
|
+
}
|
|
50
|
+
async deviceAuthorization(ctx) {
|
|
51
|
+
const auth = await this.#authenticate(ctx);
|
|
52
|
+
if (!auth)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
const body = await auth.runtime.delegation.requestDevice(auth.agent, ctx.request.input('scope'));
|
|
56
|
+
noStore(ctx);
|
|
57
|
+
return ctx.response.json(body);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof AgentOAuthError)
|
|
61
|
+
return oauthError(ctx, error);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async token(ctx) {
|
|
66
|
+
const auth = await this.#authenticate(ctx);
|
|
67
|
+
if (!auth)
|
|
68
|
+
return;
|
|
69
|
+
const delegation = auth.runtime.delegation;
|
|
70
|
+
try {
|
|
71
|
+
const grantType = ctx.request.input('grant_type');
|
|
72
|
+
const body = grantType === DEVICE_CODE_GRANT_TYPE
|
|
73
|
+
? await delegation.exchangeDeviceCode(auth.agent, ctx.request.input('device_code'))
|
|
74
|
+
: grantType === REFRESH_TOKEN_GRANT_TYPE
|
|
75
|
+
? await delegation.refresh(auth.agent, ctx.request.input('refresh_token'))
|
|
76
|
+
: null;
|
|
77
|
+
if (!body) {
|
|
78
|
+
throw new AgentOAuthError('unsupported_grant_type', 'Unsupported grant_type');
|
|
79
|
+
}
|
|
80
|
+
noStore(ctx);
|
|
81
|
+
return ctx.response.json(body);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof AgentOAuthError)
|
|
85
|
+
return oauthError(ctx, error);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* JWT do agente + `client_id` coerente. Já respondeu (401/404) quando
|
|
91
|
+
* devolve `null`.
|
|
92
|
+
*/
|
|
93
|
+
async #authenticate(ctx) {
|
|
94
|
+
const runtime = await personalAgentsFor(ctx);
|
|
95
|
+
if (!runtime?.delegation) {
|
|
96
|
+
ctx.response.notFound();
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const agent = await runtime.verifier.verify(ctx.request.header('authorization'));
|
|
100
|
+
if (!agent) {
|
|
101
|
+
personalAgentUnauthorized(ctx, runtime.config.protocol);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (ctx.request.input('client_id') !== agent.issuer) {
|
|
105
|
+
oauthError(ctx, new AgentOAuthError('invalid_client', 'client_id must equal the personal-agent issuer', 401));
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return { runtime, agent };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
import { type CustomLoginIdentity } from '../custom_login.js';
|
|
3
4
|
export default class AuthInteractionController {
|
|
4
5
|
#private;
|
|
6
|
+
/** Shared completion for trusted host authentication; primary proof is the host's responsibility. */
|
|
7
|
+
completeCustomLogin(ctx: HttpContext, input: CustomLoginIdentity & {
|
|
8
|
+
method: string;
|
|
9
|
+
factorId?: string;
|
|
10
|
+
passwordless?: boolean;
|
|
11
|
+
}): Promise<unknown>;
|
|
5
12
|
show(ctx: HttpContext): Promise<any>;
|
|
6
13
|
/**
|
|
7
14
|
* POST /auth/interaction/:uid/identifier
|
|
@@ -29,7 +36,12 @@ export default class AuthInteractionController {
|
|
|
29
36
|
* 2º fator: lê o accountId pendente da sessão e aceita um código TOTP (`code`)
|
|
30
37
|
* OU um recovery code (`recoveryCode`). Em caso de sucesso finaliza a interaction.
|
|
31
38
|
*/
|
|
32
|
-
|
|
39
|
+
private renderMfaChallenge;
|
|
40
|
+
private configuredMfaGate;
|
|
41
|
+
customMfaBegin(ctx: HttpContext): Promise<unknown>;
|
|
42
|
+
customMfaVerify(ctx: HttpContext): Promise<unknown>;
|
|
43
|
+
private finishVerifiedMfa;
|
|
44
|
+
mfaVerify(ctx: HttpContext): Promise<unknown>;
|
|
33
45
|
/**
|
|
34
46
|
* true se o authorize request exige MFA via acr_values (contém o mfaAcr da
|
|
35
47
|
* config de step-up). `acr_values` é a string separada por espaços padrão OIDC.
|