@adonis-agora/authkit-server 0.74.0 → 0.75.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/src/define_config.d.ts +8 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/access_token_verifier.d.ts +7 -0
- package/build/src/host/access_token_verifier.js +8 -0
- package/build/src/host/bearer_account.d.ts +17 -0
- package/build/src/host/bearer_account.js +10 -0
- package/build/src/host/console_session.js +3 -5
- package/build/src/host/impersonation_session.d.ts +5 -0
- package/build/src/host/impersonation_session.js +37 -4
- package/build/src/host/oidc_bearer_guard.js +10 -1
- package/build/src/provider/build_provider.js +5 -0
- package/build/src/provider/oidc_service.js +1 -0
- package/build/src/provider/token_exchange.d.ts +26 -0
- package/build/src/provider/token_exchange.js +44 -3
- package/package.json +1 -1
|
@@ -622,11 +622,19 @@ export interface AdminConfigInput {
|
|
|
622
622
|
* não altera mais o valor.
|
|
623
623
|
*/
|
|
624
624
|
impersonation?: boolean;
|
|
625
|
+
/**
|
|
626
|
+
* Permite impersonar uma conta que também tem um dos `roles` de admin. Com
|
|
627
|
+
* `false`, o token-exchange recusa alvo admin (`invalid_grant`): um admin não
|
|
628
|
+
* assume a identidade de outro. Default: **`true`** (back-compat; virar o
|
|
629
|
+
* default é decisão de major, mesma regra de `impersonation`).
|
|
630
|
+
*/
|
|
631
|
+
impersonateAdmins?: boolean;
|
|
625
632
|
}
|
|
626
633
|
export interface ResolvedAdminConfig {
|
|
627
634
|
enabled: boolean;
|
|
628
635
|
roles: string[];
|
|
629
636
|
impersonation: boolean;
|
|
637
|
+
impersonateAdmins: boolean;
|
|
630
638
|
}
|
|
631
639
|
export declare function resolveAdmin(input?: AdminConfigInput): ResolvedAdminConfig;
|
|
632
640
|
/**
|
|
@@ -174,6 +174,7 @@ export function resolveAdmin(input) {
|
|
|
174
174
|
// Default `true`: preserva o comportamento histórico (grant sempre
|
|
175
175
|
// registrado). Ver o docblock de `AdminConfigInput.impersonation`.
|
|
176
176
|
impersonation: input?.impersonation !== false,
|
|
177
|
+
impersonateAdmins: input?.impersonateAdmins !== false,
|
|
177
178
|
};
|
|
178
179
|
}
|
|
179
180
|
/** Lê API keys de `AUTHKIT_ADMIN_API_KEY` (uma ou várias, separadas por vírgula). */
|
|
@@ -18,6 +18,13 @@ export interface VerifiedAccessToken {
|
|
|
18
18
|
exp: number | null;
|
|
19
19
|
/** Id do token (`jti`), quando conhecido. */
|
|
20
20
|
jti: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Quem está AGINDO quando o token é de impersonation (RFC 8693 `act.sub`, o
|
|
23
|
+
* admin que trocou o próprio token pelo do `sub`). `null` num token comum.
|
|
24
|
+
* É o que deixa o resource server tratar a request como impersonation
|
|
25
|
+
* (`impersonationState`/`realAccountId`) em vez de confundi-la com o alvo.
|
|
26
|
+
*/
|
|
27
|
+
actor: string | null;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Estratégia de verificação de access token plugável no `oidcBearerGuard`.
|
|
@@ -10,6 +10,11 @@ function rejectedJwt(error) {
|
|
|
10
10
|
!(error instanceof joseErrors.JWKSTimeout) &&
|
|
11
11
|
!(error instanceof joseErrors.JWKSInvalid));
|
|
12
12
|
}
|
|
13
|
+
/** `act.sub` (RFC 8693 §4.1) de um payload/`extra`/introspecção, ou `null`. */
|
|
14
|
+
function actorOf(source) {
|
|
15
|
+
const act = source?.act;
|
|
16
|
+
return typeof act?.sub === 'string' && act.sub ? act.sub : null;
|
|
17
|
+
}
|
|
13
18
|
/** `header.payload.signature` em base64url — o formato compacto de um JWS. */
|
|
14
19
|
const JWS_COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
15
20
|
function looksLikeJwt(token) {
|
|
@@ -44,6 +49,7 @@ function fromJwtPayload(payload) {
|
|
|
44
49
|
audience: toAudience(payload.aud),
|
|
45
50
|
exp: typeof payload.exp === 'number' ? payload.exp : null,
|
|
46
51
|
jti: typeof payload.jti === 'string' ? payload.jti : null,
|
|
52
|
+
actor: actorOf(payload),
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
@@ -121,6 +127,7 @@ export function inProcessAccessTokenVerifier(resolveIssuer) {
|
|
|
121
127
|
audience: toAudience(at.aud),
|
|
122
128
|
exp: typeof at.exp === 'number' ? at.exp : null,
|
|
123
129
|
jti: typeof at.jti === 'string' ? at.jti : null,
|
|
130
|
+
actor: actorOf(at.extra),
|
|
124
131
|
};
|
|
125
132
|
},
|
|
126
133
|
async issue(accountId, options = {}) {
|
|
@@ -212,6 +219,7 @@ export function remoteAccessTokenVerifier(options) {
|
|
|
212
219
|
audience: toAudience(body.aud),
|
|
213
220
|
exp: typeof body.exp === 'number' ? body.exp : null,
|
|
214
221
|
jti: typeof body.jti === 'string' ? body.jti : null,
|
|
222
|
+
actor: actorOf(body),
|
|
215
223
|
};
|
|
216
224
|
};
|
|
217
225
|
return {
|
|
@@ -7,3 +7,20 @@ export declare function clearBearerAccountId(ctx: object): void;
|
|
|
7
7
|
* `oidcBearerGuard` autenticou (ainda) a request.
|
|
8
8
|
*/
|
|
9
9
|
export declare function bearerAccountId(ctx: object): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Impersonation carregada pelo access token bearer desta request: o ator
|
|
12
|
+
* (`act.sub`, o admin) e a expiração do token. Gravada pelo `oidcBearerGuard`
|
|
13
|
+
* só quando o token é de impersonation; lida por `impersonationState` e
|
|
14
|
+
* `realAccountId` quando não há sessão.
|
|
15
|
+
*/
|
|
16
|
+
export interface BearerImpersonation {
|
|
17
|
+
actorId: string;
|
|
18
|
+
/** Epoch em segundos do `exp` do token, quando conhecido. */
|
|
19
|
+
exp: number | null;
|
|
20
|
+
/** `jti` do token trocado, quando conhecido (correlaciona com a auditoria). */
|
|
21
|
+
jti: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** Registra que o token bearer desta request é de impersonation. */
|
|
24
|
+
export declare function setBearerImpersonation(ctx: object, value: BearerImpersonation): void;
|
|
25
|
+
/** Impersonation do token bearer desta request, ou `null`. */
|
|
26
|
+
export declare function bearerImpersonation(ctx: object): BearerImpersonation | null;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* barrel) depende dele.
|
|
12
12
|
*/
|
|
13
13
|
const bearerAccounts = new WeakMap();
|
|
14
|
+
const bearerImpersonations = new WeakMap();
|
|
14
15
|
/** Registra a conta autenticada via bearer nesta request. */
|
|
15
16
|
export function setBearerAccountId(ctx, accountId) {
|
|
16
17
|
bearerAccounts.set(ctx, accountId);
|
|
@@ -18,6 +19,7 @@ export function setBearerAccountId(ctx, accountId) {
|
|
|
18
19
|
/** Esquece a conta bearer desta request (falha de autenticação). */
|
|
19
20
|
export function clearBearerAccountId(ctx) {
|
|
20
21
|
bearerAccounts.delete(ctx);
|
|
22
|
+
bearerImpersonations.delete(ctx);
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Id da conta autenticada via bearer nesta request, ou `null` quando nenhum
|
|
@@ -26,3 +28,11 @@ export function clearBearerAccountId(ctx) {
|
|
|
26
28
|
export function bearerAccountId(ctx) {
|
|
27
29
|
return bearerAccounts.get(ctx) ?? null;
|
|
28
30
|
}
|
|
31
|
+
/** Registra que o token bearer desta request é de impersonation. */
|
|
32
|
+
export function setBearerImpersonation(ctx, value) {
|
|
33
|
+
bearerImpersonations.set(ctx, value);
|
|
34
|
+
}
|
|
35
|
+
/** Impersonation do token bearer desta request, ou `null`. */
|
|
36
|
+
export function bearerImpersonation(ctx) {
|
|
37
|
+
return bearerImpersonations.get(ctx) ?? null;
|
|
38
|
+
}
|
|
@@ -65,11 +65,9 @@ export function getAccountId(ctx) {
|
|
|
65
65
|
* if (!id || !(await authz.hasRole(id, 'admin'))) throw new Error('forbidden')
|
|
66
66
|
*/
|
|
67
67
|
export function realAccountId(ctx) {
|
|
68
|
-
//
|
|
69
|
-
// `
|
|
70
|
-
//
|
|
71
|
-
if (!ctx.session)
|
|
72
|
-
return bearerAccountId(ctx);
|
|
68
|
+
// `impersonationState` cobre as duas fontes: a sessão do console e o access
|
|
69
|
+
// token bearer trocado (`act`, ex.: app nativo) — nos dois, o humano real é o
|
|
70
|
+
// impersonator. Sem impersonation, a conta da sessão ou do bearer.
|
|
73
71
|
return impersonationState(ctx).impersonatorId ?? getAccountId(ctx);
|
|
74
72
|
}
|
|
75
73
|
/**
|
|
@@ -61,6 +61,11 @@ export interface ImpersonationState {
|
|
|
61
61
|
* do lado do IdP, NÃO impõe expiração na sessão (só `maxAge` faz isso).
|
|
62
62
|
*/
|
|
63
63
|
exchangeExpiresIn?: number;
|
|
64
|
+
/**
|
|
65
|
+
* De onde veio a impersonation: `session` (console/web, keys de sessão) ou
|
|
66
|
+
* `bearer` (access token trocado com `act`, ex.: app nativo). Só quando `active`.
|
|
67
|
+
*/
|
|
68
|
+
source?: 'session' | 'bearer';
|
|
64
69
|
}
|
|
65
70
|
/** Metadados aproveitados da resposta do token-exchange (só o que não é segredo). */
|
|
66
71
|
export interface TokenExchangeResult {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
3
|
+
import { bearerAccountId, bearerImpersonation } from './bearer_account.js';
|
|
3
4
|
/**
|
|
4
5
|
* Ergonômico de SESSÃO de browser no RP para "personificar" (impersonate) um
|
|
5
6
|
* usuário e navegar como ele — roteado pelo token-exchange RFC 8693 que o IdP já
|
|
@@ -304,11 +305,16 @@ export async function startImpersonation(ctx, params) {
|
|
|
304
305
|
* false }` puro quando nunca houve impersonation.
|
|
305
306
|
*/
|
|
306
307
|
export function impersonationState(ctx) {
|
|
307
|
-
const impersonatorId = ctx.session
|
|
308
|
-
if (!impersonatorId)
|
|
309
|
-
|
|
308
|
+
const impersonatorId = ctx.session?.get(IMPERSONATOR_SESSION_KEY);
|
|
309
|
+
if (!impersonatorId) {
|
|
310
|
+
// Sessão de console logada manda (mesma regra do `getAccountId`): o bearer só
|
|
311
|
+
// conta quando a request não tem conta na sessão.
|
|
312
|
+
if (ctx.session?.get(ACCOUNT_SESSION_KEY))
|
|
313
|
+
return { active: false };
|
|
314
|
+
return bearerImpersonationState(ctx);
|
|
315
|
+
}
|
|
310
316
|
const targetId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
311
|
-
const state = { active: true, targetId, impersonatorId };
|
|
317
|
+
const state = { active: true, targetId, impersonatorId, source: 'session' };
|
|
312
318
|
const impersonationId = ctx.session.get(IMPERSONATION_ID_SESSION_KEY);
|
|
313
319
|
if (impersonationId)
|
|
314
320
|
state.impersonationId = impersonationId;
|
|
@@ -329,6 +335,33 @@ export function impersonationState(ctx) {
|
|
|
329
335
|
}
|
|
330
336
|
return state;
|
|
331
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Impersonation pelo ACCESS TOKEN (sem sessão): o `oidcBearerGuard` autenticou
|
|
340
|
+
* um token trocado que carrega `act`. O alvo é o `sub` do token; o impersonator,
|
|
341
|
+
* o `act.sub`; a expiração, o `exp` do token (o token trocado não tem refresh).
|
|
342
|
+
* Só existe depois que o guard bearer rodou na request, como o `getAccountId`.
|
|
343
|
+
*/
|
|
344
|
+
function bearerImpersonationState(ctx) {
|
|
345
|
+
const bearer = bearerImpersonation(ctx);
|
|
346
|
+
const targetId = bearerAccountId(ctx);
|
|
347
|
+
if (!bearer || !targetId)
|
|
348
|
+
return { active: false };
|
|
349
|
+
const state = {
|
|
350
|
+
active: true,
|
|
351
|
+
targetId,
|
|
352
|
+
impersonatorId: bearer.actorId,
|
|
353
|
+
actSub: bearer.actorId,
|
|
354
|
+
source: 'bearer',
|
|
355
|
+
};
|
|
356
|
+
if (bearer.jti)
|
|
357
|
+
state.impersonationId = bearer.jti;
|
|
358
|
+
if (bearer.exp !== null) {
|
|
359
|
+
state.expiresAt = bearer.exp * 1000;
|
|
360
|
+
if (Date.now() > state.expiresAt)
|
|
361
|
+
state.active = false;
|
|
362
|
+
}
|
|
363
|
+
return state;
|
|
364
|
+
}
|
|
332
365
|
/**
|
|
333
366
|
* Encerra a impersonation: restaura `account_user_id = impersonator`, remove
|
|
334
367
|
* TODAS as keys de sessão de impersonation (id, tempos, prova do exchange) e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
2
2
|
import { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './access_token_verifier.js';
|
|
3
|
-
import { clearBearerAccountId, setBearerAccountId } from './bearer_account.js';
|
|
3
|
+
import { clearBearerAccountId, setBearerAccountId, setBearerImpersonation, } from './bearer_account.js';
|
|
4
4
|
import { cachedUnauthorizedAccessConstructor, loadUnauthorizedAccess, } from './oidc_rp_guard.js';
|
|
5
5
|
/**
|
|
6
6
|
* Driver do `E_UNAUTHORIZED_ACCESS` lançado pelo guard. `access_tokens` é o
|
|
@@ -144,10 +144,19 @@ export class OidcBearerGuard {
|
|
|
144
144
|
const guardUser = await this.#userProvider.findById(token.sub);
|
|
145
145
|
if (!guardUser)
|
|
146
146
|
throw this.#fail('invalid_token');
|
|
147
|
+
// Token de impersonation: o ator (o admin) também tem que existir AGORA. Conta
|
|
148
|
+
// apagada/desativada depois da troca derruba o acesso na próxima request, sem
|
|
149
|
+
// esperar o `exp` do token.
|
|
150
|
+
if (token.actor && !(await this.#userProvider.findById(token.actor))) {
|
|
151
|
+
throw this.#fail('invalid_token');
|
|
152
|
+
}
|
|
147
153
|
this.user = guardUser.getOriginal();
|
|
148
154
|
this.accessToken = token;
|
|
149
155
|
this.isAuthenticated = true;
|
|
150
156
|
setBearerAccountId(this.#ctx, String(guardUser.getId()));
|
|
157
|
+
if (token.actor) {
|
|
158
|
+
setBearerImpersonation(this.#ctx, { actorId: token.actor, exp: token.exp, jti: token.jti });
|
|
159
|
+
}
|
|
151
160
|
this.#emitter.emit('oidc_bearer:authentication_succeeded', {
|
|
152
161
|
ctx: this.#ctx,
|
|
153
162
|
guardName: this.#name,
|
|
@@ -5,6 +5,7 @@ import { assertClientMetadata } from '../host/client_metadata.js';
|
|
|
5
5
|
import { createDeviceSources } from './device_sources.js';
|
|
6
6
|
import { createLogoutSources } from './logout_sources.js';
|
|
7
7
|
import { registrationPolicyMiddleware } from './registration_policy.js';
|
|
8
|
+
import { impersonationExtraClaims } from './token_exchange.js';
|
|
8
9
|
/** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
|
|
9
10
|
export function updateSessionTtlHolder(holder, policy) {
|
|
10
11
|
holder.rememberSec = Math.max(1, Math.floor(policy.rememberDays * 86400));
|
|
@@ -118,6 +119,10 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
|
|
|
118
119
|
}
|
|
119
120
|
: {};
|
|
120
121
|
const provider = new oidc.Provider(config.issuer, {
|
|
122
|
+
// O ator (`act`, RFC 8693) dos access tokens de impersonation — ver
|
|
123
|
+
// `impersonationExtraClaims` em `token_exchange.ts`. Sem isto o token trocado
|
|
124
|
+
// não se distingue de um token do próprio alvo no resource server.
|
|
125
|
+
extraTokenClaims: async (_ctx, token) => impersonationExtraClaims(token),
|
|
121
126
|
// Dispatcher por modelo (suportado pelo oidc-provider: `Adapter` aceita
|
|
122
127
|
// função `(name) => adapter` além de classe). Session-scoped vai pro
|
|
123
128
|
// `SessionAdapterClass`, o resto pro `AdapterClass` — mesma regra de
|
|
@@ -181,6 +181,7 @@ export class OidcService {
|
|
|
181
181
|
// conta que acabou de desabilitar, recebendo tokens plenamente funcionais.
|
|
182
182
|
accountStore: config.accountStore,
|
|
183
183
|
audit: config.audit,
|
|
184
|
+
impersonateAdmins: config.admin.impersonateAdmins,
|
|
184
185
|
});
|
|
185
186
|
}
|
|
186
187
|
// Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
|
|
@@ -56,5 +56,31 @@ export interface TokenExchangeDeps {
|
|
|
56
56
|
* regra de todo o resto da lib — nunca quebra hosts com um store mínimo).
|
|
57
57
|
*/
|
|
58
58
|
accountStore?: AccountStore;
|
|
59
|
+
/**
|
|
60
|
+
* Permite impersonar uma conta que TAMBÉM tem um dos `adminRoles`. Default
|
|
61
|
+
* `true` (back-compat). Com `false`, alvo admin vira `invalid_grant`: um admin
|
|
62
|
+
* não assume a identidade (e os tokens) de outro admin.
|
|
63
|
+
*/
|
|
64
|
+
impersonateAdmins?: boolean;
|
|
59
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Propriedade (não persistida) onde a troca marca o ator ANTES do `save()`. O
|
|
68
|
+
* `extraTokenClaims` do provider (`build_provider.ts`) a transforma em
|
|
69
|
+
* `extra.act` — é o único jeito de pôr claims no `extra` de um token opaco: o
|
|
70
|
+
* oidc-provider SOBRESCREVE `extra` com o retorno desse hook ao salvar.
|
|
71
|
+
*/
|
|
72
|
+
export declare const IMPERSONATION_ACTOR_PROP = "authkitImpersonationActor";
|
|
73
|
+
/** `extraTokenClaims` do provider: `{ act }` para token de impersonation, nada para o resto. */
|
|
74
|
+
export declare function impersonationExtraClaims(token: unknown): {
|
|
75
|
+
act: {
|
|
76
|
+
sub: string;
|
|
77
|
+
};
|
|
78
|
+
} | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* O ator de um access token de impersonation: o `act` (RFC 8693 §4.1) gravado
|
|
81
|
+
* no `extra` do token trocado. `null` quando o token não é de impersonation.
|
|
82
|
+
*/
|
|
83
|
+
export declare function impersonationActorOf(token: {
|
|
84
|
+
extra?: unknown;
|
|
85
|
+
} | null | undefined): string | null;
|
|
60
86
|
export declare function registerTokenExchange(provider: any, deps: TokenExchangeDeps): void;
|
|
@@ -2,6 +2,26 @@ import { errors } from 'oidc-provider';
|
|
|
2
2
|
import { assertAccountEnabled } from '../host/login_attempt.js';
|
|
3
3
|
const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
|
|
4
4
|
const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
|
|
5
|
+
/**
|
|
6
|
+
* Propriedade (não persistida) onde a troca marca o ator ANTES do `save()`. O
|
|
7
|
+
* `extraTokenClaims` do provider (`build_provider.ts`) a transforma em
|
|
8
|
+
* `extra.act` — é o único jeito de pôr claims no `extra` de um token opaco: o
|
|
9
|
+
* oidc-provider SOBRESCREVE `extra` com o retorno desse hook ao salvar.
|
|
10
|
+
*/
|
|
11
|
+
export const IMPERSONATION_ACTOR_PROP = 'authkitImpersonationActor';
|
|
12
|
+
/** `extraTokenClaims` do provider: `{ act }` para token de impersonation, nada para o resto. */
|
|
13
|
+
export function impersonationExtraClaims(token) {
|
|
14
|
+
const actor = token?.[IMPERSONATION_ACTOR_PROP];
|
|
15
|
+
return typeof actor === 'string' && actor ? { act: { sub: actor } } : undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* O ator de um access token de impersonation: o `act` (RFC 8693 §4.1) gravado
|
|
19
|
+
* no `extra` do token trocado. `null` quando o token não é de impersonation.
|
|
20
|
+
*/
|
|
21
|
+
export function impersonationActorOf(token) {
|
|
22
|
+
const act = token?.extra?.act;
|
|
23
|
+
return typeof act?.sub === 'string' && act.sub ? act.sub : null;
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Interseção entre os scopes pedidos e os scopes permitidos do client (allowlist).
|
|
7
27
|
* Preserva a ordem do pedido. Nunca excede a allowlist do client.
|
|
@@ -30,6 +50,12 @@ export function registerTokenExchange(provider, deps) {
|
|
|
30
50
|
if (!subjectAt || subjectAt.isExpired) {
|
|
31
51
|
throw new errors.InvalidGrant('subject_token invalid or expired');
|
|
32
52
|
}
|
|
53
|
+
// Sem impersonation encadeada: um token que JÁ é de impersonation não vira
|
|
54
|
+
// subject de outra troca (o "ator" seria a conta personificada, e a trilha
|
|
55
|
+
// perderia quem de fato está agindo).
|
|
56
|
+
if (impersonationActorOf(subjectAt)) {
|
|
57
|
+
throw new errors.InvalidGrant('subject_token is already an impersonation token');
|
|
58
|
+
}
|
|
33
59
|
// O subject_token DEVE ter sido emitido para o MESMO client autenticado: senão
|
|
34
60
|
// um client B poderia trocar um AT emitido para o client A (cross-client).
|
|
35
61
|
if (subjectAt.clientId !== client?.clientId) {
|
|
@@ -62,6 +88,9 @@ export function registerTokenExchange(provider, deps) {
|
|
|
62
88
|
if (!target) {
|
|
63
89
|
throw new errors.InvalidGrant('requested_subject not found');
|
|
64
90
|
}
|
|
91
|
+
if (target.id === actor.id) {
|
|
92
|
+
throw new errors.InvalidGrant('requested_subject must be another account');
|
|
93
|
+
}
|
|
65
94
|
// Status do alvo (disabled/expirado): mesmo gate de `attemptPasswordLogin`.
|
|
66
95
|
// Sem isso, impersonar um alvo desabilitado mintava tokens funcionais para
|
|
67
96
|
// uma identidade que o admin acreditava ter revogado. `accountStore` é
|
|
@@ -107,8 +136,6 @@ export function registerTokenExchange(provider, deps) {
|
|
|
107
136
|
// Client sem allowlist declarada: comportamento atual preservado.
|
|
108
137
|
scope = params.scope || DEFAULT_SCOPE;
|
|
109
138
|
}
|
|
110
|
-
const at = new provider.AccessToken({ accountId: target.id, client, scope });
|
|
111
|
-
const accessToken = await at.save();
|
|
112
139
|
// Token exchange is not tied to a browser session, so there is no active org
|
|
113
140
|
// context here — roles are resolved for the impersonated target with clientId only.
|
|
114
141
|
const roles = deps.resolveTokenRoles
|
|
@@ -117,6 +144,17 @@ export function registerTokenExchange(provider, deps) {
|
|
|
117
144
|
activeOrg: null,
|
|
118
145
|
})
|
|
119
146
|
: (target.globalRoles ?? []);
|
|
147
|
+
if (deps.impersonateAdmins === false && roles.some((r) => adminRoles.includes(r))) {
|
|
148
|
+
throw new errors.InvalidGrant('requested_subject is not impersonable');
|
|
149
|
+
}
|
|
150
|
+
// O ACCESS token (não só o id_token) carrega o ator: é o que o resource server
|
|
151
|
+
// vê a cada request (`oidcBearerGuard` → `impersonationState`/`realAccountId`).
|
|
152
|
+
// Sem isso, o token trocado era indistinguível de um token do próprio alvo, e
|
|
153
|
+
// toda regra "negado durante impersonation" deixava passar o app nativo.
|
|
154
|
+
// `extra` sai no JWT (formato jwt) e na introspecção (RFC 7662).
|
|
155
|
+
const at = new provider.AccessToken({ accountId: target.id, client, scope });
|
|
156
|
+
at[IMPERSONATION_ACTOR_PROP] = actor.id;
|
|
157
|
+
const accessToken = await at.save();
|
|
120
158
|
const idToken = new provider.IdToken({
|
|
121
159
|
sub: target.id,
|
|
122
160
|
email: target.email,
|
|
@@ -134,7 +172,7 @@ export function registerTokenExchange(provider, deps) {
|
|
|
134
172
|
email: target.email ?? null,
|
|
135
173
|
clientId: client?.clientId ?? null,
|
|
136
174
|
ip: ctx.req?.socket?.remoteAddress ?? null,
|
|
137
|
-
metadata: { scope },
|
|
175
|
+
metadata: { scope, jti: at.jti ?? null },
|
|
138
176
|
});
|
|
139
177
|
ctx.body = {
|
|
140
178
|
access_token: accessToken,
|
|
@@ -143,6 +181,9 @@ export function registerTokenExchange(provider, deps) {
|
|
|
143
181
|
expires_in: at.expiration ?? 3600,
|
|
144
182
|
id_token: idTokenJwt,
|
|
145
183
|
scope,
|
|
184
|
+
// RFC 8693 §4.1: o ator também no corpo da resposta (o `actSub` que o
|
|
185
|
+
// `requestTokenExchange` do host lê — antes ficava sempre vazio).
|
|
186
|
+
act: { sub: actor.id },
|
|
146
187
|
};
|
|
147
188
|
};
|
|
148
189
|
provider.registerGrantType(TOKEN_EXCHANGE, handler, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.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",
|