@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.
|
|
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",
|