@adonis-agora/authkit-server 0.39.0 → 0.40.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/build/index.d.ts CHANGED
@@ -111,6 +111,8 @@ export { resolveRegistration } from './src/define_config.js';
111
111
  export type { RegistrationConfigInput, ResolvedRegistrationConfig, } from './src/define_config.js';
112
112
  export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
113
113
  export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
114
+ export { rememberAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
115
+ export type { StartImpersonationParams, ImpersonationState, } from './src/host/impersonation_session.js';
114
116
  export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
115
117
  export type { SudoModeSetting, ResolvedSudoModeSetting, } from './src/host/sudo_mode.js';
116
118
  export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
package/build/index.js CHANGED
@@ -72,6 +72,10 @@ export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVeri
72
72
  export { resolveRegistration } from './src/define_config.js';
73
73
  export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
74
74
  export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
75
+ // Session impersonation — RP-side glue that routes through the IdP's RFC 8693
76
+ // token-exchange (the IdP validates the admin role + audits). See
77
+ // src/host/impersonation_session.ts.
78
+ export { rememberAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
75
79
  // Sudo mode — helpers for host controllers that require step-up authentication.
76
80
  export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
77
81
  // @adonisjs/auth integration (opt-in) — user provider for config/auth.ts's
@@ -0,0 +1,51 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * Guarda o access token do admin na sessão (chame no callback OIDC do RP, logo
4
+ * após o login). Necessário porque o token-exchange exige o access token do admin
5
+ * como `subject_token`, e o RP normalmente descarta os tokens após o login.
6
+ *
7
+ * Access tokens são curtos: se expirar, `startImpersonation` falha e o admin
8
+ * re-loga (aceitável; refresh fica pra depois — YAGNI).
9
+ */
10
+ export declare function rememberAccessToken(ctx: HttpContext, accessToken: string): void;
11
+ export interface StartImpersonationParams {
12
+ /** Id do usuário-alvo a personificar. */
13
+ targetId: string;
14
+ /** Issuer do IdP (ex.: http://localhost:3333/oidc). */
15
+ issuer: string;
16
+ clientId: string;
17
+ clientSecret?: string;
18
+ /** Token endpoint do IdP (via `discoverEndpoints`). Default: `${issuer}/token`. */
19
+ tokenEndpoint?: string;
20
+ scope?: string;
21
+ fetchImpl?: typeof fetch;
22
+ }
23
+ export interface ImpersonationState {
24
+ active: boolean;
25
+ /** = `account_user_id` atual, quando `active`. */
26
+ targetId?: string;
27
+ /** O admin real (impersonator), quando `active`. */
28
+ impersonatorId?: string;
29
+ }
30
+ /**
31
+ * Inicia a impersonation: lê o access token do admin da sessão, chama o
32
+ * token-exchange (o IdP valida a role admin e audita) e SÓ em sucesso guarda o
33
+ * impersonator (= `account_user_id` atual), regenera a sessão (anti-fixation) e
34
+ * seta `account_user_id = targetId`.
35
+ *
36
+ * - Se o exchange falhar, LANÇA e NÃO troca NADA na sessão.
37
+ * - Recusa (lança) se já houver impersonation ativa (pare a atual antes).
38
+ * - Recusa (lança) se não houver access token do admin na sessão.
39
+ */
40
+ export declare function startImpersonation(ctx: HttpContext, params: StartImpersonationParams): Promise<void>;
41
+ /**
42
+ * Estado da impersonation pra UI (ex.: banner). `active` quando há um impersonator
43
+ * guardado na sessão.
44
+ */
45
+ export declare function impersonationState(ctx: HttpContext): ImpersonationState;
46
+ /**
47
+ * Encerra a impersonation: restaura `account_user_id = impersonator`, limpa TODAS
48
+ * as keys de impersonation (impersonator + access token do admin) e regenera a
49
+ * sessão (anti-fixation). No-op quando não há impersonation ativa.
50
+ */
51
+ export declare function stopImpersonation(ctx: HttpContext): Promise<void>;
@@ -0,0 +1,128 @@
1
+ import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
2
+ /**
3
+ * Ergonômico de SESSÃO de browser no RP para "personificar" (impersonate) um
4
+ * usuário e navegar como ele — roteado pelo token-exchange RFC 8693 que o IdP já
5
+ * expõe (`provider/token_exchange.ts`). Assim a impersonation herda o AUDIT
6
+ * central do IdP + o claim `act` no token, em vez de o app colar isso na mão.
7
+ *
8
+ * INVARIANTE DE SEGURANÇA: este helper é só glue de sessão — a AUTORIZAÇÃO é do
9
+ * IdP. O token-exchange REJEITA um `subject_token` que não seja de um admin, então
10
+ * `startImpersonation` só troca a sessão quando o exchange retorna 2xx. NÃO
11
+ * reimplementamos checagem de role aqui.
12
+ *
13
+ * A troca acontece na MESMA key de sessão que a identidade da conta do console
14
+ * (`account_user_id`, via `ACCOUNT_SESSION_KEY`) — o resto do app (middleware,
15
+ * `getAccountId`) continua funcionando sem saber que há impersonation ativa.
16
+ */
17
+ const TOKEN_EXCHANGE_GRANT = 'urn:ietf:params:oauth:grant-type:token-exchange';
18
+ const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
19
+ /**
20
+ * Key de sessão que guarda o admin REAL (o impersonator) enquanto há uma
21
+ * impersonation ativa. Interna: NÃO exporte o literal — é contrato de
22
+ * implementação, leia via `impersonationState`.
23
+ */
24
+ const IMPERSONATOR_SESSION_KEY = 'impersonator_user_id';
25
+ /**
26
+ * Key de sessão que guarda o access token do admin, necessário como
27
+ * `subject_token` do token-exchange. Interna: NÃO exporte o literal.
28
+ */
29
+ const ADMIN_ACCESS_TOKEN_SESSION_KEY = 'admin_access_token';
30
+ /**
31
+ * Guarda o access token do admin na sessão (chame no callback OIDC do RP, logo
32
+ * após o login). Necessário porque o token-exchange exige o access token do admin
33
+ * como `subject_token`, e o RP normalmente descarta os tokens após o login.
34
+ *
35
+ * Access tokens são curtos: se expirar, `startImpersonation` falha e o admin
36
+ * re-loga (aceitável; refresh fica pra depois — YAGNI).
37
+ */
38
+ export function rememberAccessToken(ctx, accessToken) {
39
+ ctx.session.put(ADMIN_ACCESS_TOKEN_SESSION_KEY, accessToken);
40
+ }
41
+ /**
42
+ * POST inline do RFC 8693 token-exchange. Inline (em vez de depender de
43
+ * `@adonis-agora/authkit-client`) porque o client NÃO é dependência do server e
44
+ * adicioná-la inverteria a direção do grafo de pacotes (server = IdP toolkit). São
45
+ * ~12 linhas; testável via `fetchImpl`. Lança se o IdP não responder 2xx (é o
46
+ * gatekeeper: não-admin / token expirado ⇒ erro ⇒ a sessão não é tocada).
47
+ */
48
+ async function requestTokenExchange(params, subjectToken) {
49
+ const body = new URLSearchParams({
50
+ grant_type: TOKEN_EXCHANGE_GRANT,
51
+ subject_token: subjectToken,
52
+ subject_token_type: ACCESS_TOKEN_TYPE,
53
+ requested_subject: params.targetId,
54
+ client_id: params.clientId,
55
+ });
56
+ if (params.scope)
57
+ body.set('scope', params.scope);
58
+ if (params.clientSecret)
59
+ body.set('client_secret', params.clientSecret);
60
+ const fetchImpl = params.fetchImpl ?? fetch;
61
+ const res = await fetchImpl(params.tokenEndpoint ?? `${params.issuer}/token`, {
62
+ method: 'POST',
63
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
64
+ body: body.toString(),
65
+ });
66
+ if (!res.ok) {
67
+ // NUNCA logamos tokens nem o corpo (pode ecoar segredos). Só o status.
68
+ throw new Error(`Token exchange failed: ${res.status}`);
69
+ }
70
+ }
71
+ /**
72
+ * Inicia a impersonation: lê o access token do admin da sessão, chama o
73
+ * token-exchange (o IdP valida a role admin e audita) e SÓ em sucesso guarda o
74
+ * impersonator (= `account_user_id` atual), regenera a sessão (anti-fixation) e
75
+ * seta `account_user_id = targetId`.
76
+ *
77
+ * - Se o exchange falhar, LANÇA e NÃO troca NADA na sessão.
78
+ * - Recusa (lança) se já houver impersonation ativa (pare a atual antes).
79
+ * - Recusa (lança) se não houver access token do admin na sessão.
80
+ */
81
+ export async function startImpersonation(ctx, params) {
82
+ if (ctx.session.get(IMPERSONATOR_SESSION_KEY)) {
83
+ throw new Error('Impersonation already active; stop the current one before starting another');
84
+ }
85
+ const adminAccessToken = ctx.session.get(ADMIN_ACCESS_TOKEN_SESSION_KEY);
86
+ if (!adminAccessToken) {
87
+ throw new Error('No admin access token in session; call rememberAccessToken after login');
88
+ }
89
+ const impersonatorId = ctx.session.get(ACCOUNT_SESSION_KEY);
90
+ if (!impersonatorId) {
91
+ // Não há admin logado para impersonar como — sem identidade para restaurar
92
+ // depois. Recusa antes de qualquer chamada/mutação.
93
+ throw new Error('No account session; log in as the admin before impersonating');
94
+ }
95
+ // O IdP é o gatekeeper: lança se o admin não puder personificar. Chamado ANTES
96
+ // de qualquer mutação de sessão — em caso de erro nada é trocado.
97
+ await requestTokenExchange(params, adminAccessToken);
98
+ // Anti-fixation: rotaciona o id da sessão (mantém os dados) antes de gravar a
99
+ // nova identidade. Mesmo padrão do consumidor real no RP.
100
+ await ctx.session.regenerate();
101
+ ctx.session.put(IMPERSONATOR_SESSION_KEY, impersonatorId);
102
+ ctx.session.put(ACCOUNT_SESSION_KEY, params.targetId);
103
+ }
104
+ /**
105
+ * Estado da impersonation pra UI (ex.: banner). `active` quando há um impersonator
106
+ * guardado na sessão.
107
+ */
108
+ export function impersonationState(ctx) {
109
+ const impersonatorId = ctx.session.get(IMPERSONATOR_SESSION_KEY);
110
+ if (!impersonatorId)
111
+ return { active: false };
112
+ const targetId = ctx.session.get(ACCOUNT_SESSION_KEY);
113
+ return { active: true, targetId, impersonatorId };
114
+ }
115
+ /**
116
+ * Encerra a impersonation: restaura `account_user_id = impersonator`, limpa TODAS
117
+ * as keys de impersonation (impersonator + access token do admin) e regenera a
118
+ * sessão (anti-fixation). No-op quando não há impersonation ativa.
119
+ */
120
+ export async function stopImpersonation(ctx) {
121
+ const impersonatorId = ctx.session.get(IMPERSONATOR_SESSION_KEY);
122
+ if (!impersonatorId)
123
+ return;
124
+ await ctx.session.regenerate();
125
+ ctx.session.put(ACCOUNT_SESSION_KEY, impersonatorId);
126
+ ctx.session.forget(IMPERSONATOR_SESSION_KEY);
127
+ ctx.session.forget(ADMIN_ACCESS_TOKEN_SESSION_KEY);
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",