@adonis-agora/authkit-server 0.62.0 → 0.64.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 +2 -2
- package/build/index.js +1 -1
- package/build/src/adapters/factory.d.ts +18 -0
- package/build/src/adapters/factory.js +35 -0
- package/build/src/define_config.d.ts +29 -0
- package/build/src/define_config.js +6 -0
- package/build/src/host/account_api/account_api_controller.js +4 -3
- package/build/src/host/admin_clients_service.js +3 -2
- package/build/src/host/admin_sessions_service.js +11 -4
- package/build/src/host/impersonation_session.d.ts +29 -0
- package/build/src/host/impersonation_session.js +68 -10
- package/build/src/provider/build_provider.js +7 -1
- package/package.json +1 -1
package/build/index.d.ts
CHANGED
|
@@ -69,8 +69,8 @@ export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
|
|
|
69
69
|
export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
|
|
70
70
|
export type { ImpersonationClientLike, ImpersonationPanel } from './src/host/impersonation.js';
|
|
71
71
|
export { buildImpersonationPanel } from './src/host/impersonation.js';
|
|
72
|
-
export type { ImpersonationState, StartImpersonationParams, } from './src/host/impersonation_session.js';
|
|
73
|
-
export { impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
72
|
+
export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonationParams, StopImpersonationOptions, TokenExchangeResult, } from './src/host/impersonation_session.js';
|
|
73
|
+
export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
74
74
|
export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
|
|
75
75
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
76
76
|
export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
|
package/build/index.js
CHANGED
|
@@ -54,7 +54,7 @@ export { buildImpersonationPanel } from './src/host/impersonation.js';
|
|
|
54
54
|
// Session impersonation — RP-side glue that routes through the IdP's RFC 8693
|
|
55
55
|
// token-exchange (the IdP validates the admin role + audits). See
|
|
56
56
|
// src/host/impersonation_session.ts.
|
|
57
|
-
export { impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
57
|
+
export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
58
58
|
// Key rotation actions — shared between the Admin REST API controller and the SDK embedded driver.
|
|
59
59
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
60
60
|
// @adonisjs/auth integration (opt-in) — guard pra Relying Parties OIDC.
|
|
@@ -4,6 +4,24 @@ export type OidcAdapterClass = new (name: string) => OidcAdapter;
|
|
|
4
4
|
export interface AdapterFactory {
|
|
5
5
|
resolver(app: ApplicationService): Promise<OidcAdapterClass>;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
|
|
9
|
+
* login (nomes exatos que o provider passa ao adapter). Perder qualquer um
|
|
10
|
+
* deles custa no máximo um relogin — nunca um outage — então são os
|
|
11
|
+
* candidatos ao adapter da `session:` (Redis). Todo modelo fora daqui
|
|
12
|
+
* (`Client` em particular — sem ele nem a tela de login abre) fica no
|
|
13
|
+
* adapter default (durável).
|
|
14
|
+
*/
|
|
15
|
+
export declare const SESSION_SCOPED_MODELS: ReadonlySet<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Escolhe a classe de adapter pro nome do modelo: session-scoped vai pra
|
|
18
|
+
* `sessionClass`, o resto pra `defaultClass`. Pura (sem I/O) de propósito —
|
|
19
|
+
* o dispatcher do provider (`build_provider.ts`) e os serviços que instanciam
|
|
20
|
+
* adapter na mão (`AdminSessionsService`, account API) usam a MESMA regra.
|
|
21
|
+
* Sem `session.adapter` configurado as duas classes são a mesma (back-compat:
|
|
22
|
+
* tudo continua onde está hoje).
|
|
23
|
+
*/
|
|
24
|
+
export declare function pickModelAdapterClass(model: string, defaultClass: OidcAdapterClass, sessionClass: OidcAdapterClass): OidcAdapterClass;
|
|
7
25
|
export interface RedisAdapterConfig {
|
|
8
26
|
/** nome da conexão do @adonisjs/redis */
|
|
9
27
|
connection: string;
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { DatabaseAdapter } from './database_adapter.js';
|
|
2
2
|
import { RedisAdapter } from './redis_adapter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Modelos do oidc-provider de vida curta, amarrados ao ciclo da sessão de
|
|
5
|
+
* login (nomes exatos que o provider passa ao adapter). Perder qualquer um
|
|
6
|
+
* deles custa no máximo um relogin — nunca um outage — então são os
|
|
7
|
+
* candidatos ao adapter da `session:` (Redis). Todo modelo fora daqui
|
|
8
|
+
* (`Client` em particular — sem ele nem a tela de login abre) fica no
|
|
9
|
+
* adapter default (durável).
|
|
10
|
+
*/
|
|
11
|
+
export const SESSION_SCOPED_MODELS = new Set([
|
|
12
|
+
'Session',
|
|
13
|
+
'Interaction',
|
|
14
|
+
'Grant',
|
|
15
|
+
'AccessToken',
|
|
16
|
+
'RefreshToken',
|
|
17
|
+
'AuthorizationCode',
|
|
18
|
+
'DeviceCode',
|
|
19
|
+
'PushedAuthorizationRequest',
|
|
20
|
+
'BackchannelAuthenticationRequest',
|
|
21
|
+
'ClientCredentials',
|
|
22
|
+
'ReplayDetection',
|
|
23
|
+
// `RegistrationAccessToken`/`InitialAccessToken` ficam FORA de propósito:
|
|
24
|
+
// gerenciam o `Client` (rotação/reconfiguração) e precisam morar junto dele
|
|
25
|
+
// no backend durável — senão um flush do Redis orfana o gerenciamento.
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Escolhe a classe de adapter pro nome do modelo: session-scoped vai pra
|
|
29
|
+
* `sessionClass`, o resto pra `defaultClass`. Pura (sem I/O) de propósito —
|
|
30
|
+
* o dispatcher do provider (`build_provider.ts`) e os serviços que instanciam
|
|
31
|
+
* adapter na mão (`AdminSessionsService`, account API) usam a MESMA regra.
|
|
32
|
+
* Sem `session.adapter` configurado as duas classes são a mesma (back-compat:
|
|
33
|
+
* tudo continua onde está hoje).
|
|
34
|
+
*/
|
|
35
|
+
export function pickModelAdapterClass(model, defaultClass, sessionClass) {
|
|
36
|
+
return SESSION_SCOPED_MODELS.has(model) ? sessionClass : defaultClass;
|
|
37
|
+
}
|
|
3
38
|
export const adapters = {
|
|
4
39
|
/**
|
|
5
40
|
* Factory para o adapter Redis. O consumidor precisa ter o @adonisjs/redis
|
|
@@ -706,6 +706,30 @@ export interface ResolvedInteractionRecoveryConfig {
|
|
|
706
706
|
export interface AuthServerConfigInput {
|
|
707
707
|
issuer: string;
|
|
708
708
|
adapter: AdapterFactory;
|
|
709
|
+
/**
|
|
710
|
+
* Override do adapter pros modelos de vida curta (sessão e órbita dela —
|
|
711
|
+
* ver `SESSION_SCOPED_MODELS` em `adapters/factory.ts`). O `adapter` de cima
|
|
712
|
+
* continua sendo o default de TUDO (back-compat: sem `session`, nada muda);
|
|
713
|
+
* com `session.adapter`, os modelos session-scoped passam a usar ele e o
|
|
714
|
+
* resto (`Client` em particular) fica no `adapter`.
|
|
715
|
+
*
|
|
716
|
+
* O split recomendado: sessão no Redis (efêmero, TTL nativo, mesmo destino
|
|
717
|
+
* da sessão do app) e o resto no banco (durável — sem o `Client` nem a tela
|
|
718
|
+
* de login abre). Mesma connection Redis pros dois níveis de sessão é a
|
|
719
|
+
* recomendação (prefixos já namespaciam; destinos diferentes criam
|
|
720
|
+
* meio-logado e dois domínios de falha no logout), mas a lib não trava isso.
|
|
721
|
+
*
|
|
722
|
+
* ```ts
|
|
723
|
+
* defineConfig({
|
|
724
|
+
* adapter: adapters.database({ connection: 'auth' }),
|
|
725
|
+
* session: { adapter: adapters.redis({ connection: 'main' }) },
|
|
726
|
+
* // ...
|
|
727
|
+
* })
|
|
728
|
+
* ```
|
|
729
|
+
*/
|
|
730
|
+
session?: {
|
|
731
|
+
adapter?: AdapterFactory;
|
|
732
|
+
};
|
|
709
733
|
/**
|
|
710
734
|
* Clientes OIDC pré-carregados no provider ao subir. Útil para testes e
|
|
711
735
|
* migrações pontuais. Para uso em produção, gerencie clients via console admin
|
|
@@ -1041,6 +1065,11 @@ export interface AuthServerConfigInput {
|
|
|
1041
1065
|
export interface ResolvedServerConfig {
|
|
1042
1066
|
issuer: string;
|
|
1043
1067
|
AdapterClass: OidcAdapterClass;
|
|
1068
|
+
/**
|
|
1069
|
+
* Adapter dos modelos session-scoped (`SESSION_SCOPED_MODELS`). Sem
|
|
1070
|
+
* `session.adapter` no input é a MESMA classe do `AdapterClass`.
|
|
1071
|
+
*/
|
|
1072
|
+
SessionAdapterClass: OidcAdapterClass;
|
|
1044
1073
|
clients: ClientConfig[];
|
|
1045
1074
|
jwks: {
|
|
1046
1075
|
keys: Record<string, any>[];
|
|
@@ -243,6 +243,11 @@ export function jwksAutoFallbackWarning(storePath) {
|
|
|
243
243
|
export function defineConfig(config) {
|
|
244
244
|
return configProvider.create(async (app) => {
|
|
245
245
|
const AdapterClass = await config.adapter.resolver(app);
|
|
246
|
+
// `session.adapter` ausente ⇒ mesma classe do default (back-compat: o
|
|
247
|
+
// dispatcher vira identidade e tudo continua no adapter único de hoje).
|
|
248
|
+
const SessionAdapterClass = config.session?.adapter
|
|
249
|
+
? await config.session.adapter.resolver(app)
|
|
250
|
+
: AdapterClass;
|
|
246
251
|
// `jwks: 'auto'` → resolve env-aware: AUTHKIT_JWKS inline, senão managed em arquivo.
|
|
247
252
|
const jwksConfig = config.jwks === 'auto'
|
|
248
253
|
? process.env.AUTHKIT_JWKS
|
|
@@ -323,6 +328,7 @@ export function defineConfig(config) {
|
|
|
323
328
|
return {
|
|
324
329
|
issuer: config.issuer,
|
|
325
330
|
AdapterClass,
|
|
331
|
+
SessionAdapterClass,
|
|
326
332
|
clients: config.clients ?? [],
|
|
327
333
|
jwks: jwks,
|
|
328
334
|
jwksConfig,
|
|
@@ -495,9 +495,10 @@ export default class AccountApiController {
|
|
|
495
495
|
if (!target) {
|
|
496
496
|
return ctx.response.notFound(apiErr('not_found', 'Session not found.'));
|
|
497
497
|
}
|
|
498
|
-
// Revoga via adapter diretamente
|
|
499
|
-
|
|
500
|
-
const
|
|
498
|
+
// Revoga via adapter diretamente (o da sessão — `Session` é
|
|
499
|
+
// session-scoped e pode viver em backend distinto do default).
|
|
500
|
+
const SessionAdapterClass = service.config.SessionAdapterClass ?? service.config.AdapterClass;
|
|
501
|
+
const sessionAdapter = new SessionAdapterClass('Session');
|
|
501
502
|
await sessionAdapter.destroy(sessionId);
|
|
502
503
|
await cfg.audit?.record({
|
|
503
504
|
type: 'session.revoked',
|
|
@@ -19,8 +19,9 @@ export class AdminClientsService {
|
|
|
19
19
|
#adapter;
|
|
20
20
|
constructor(oidc) {
|
|
21
21
|
this.oidc = oidc;
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// `Client` NÃO é session-scoped: fica sempre no `AdapterClass` (default),
|
|
23
|
+
// os mesmos artefatos que o oidc-provider persiste — mesmo com `session:`
|
|
24
|
+
// configurado, clients continuam no backend durável.
|
|
24
25
|
this.#adapter = new oidc.config.AdapterClass('Client');
|
|
25
26
|
}
|
|
26
27
|
/** Indica se o adapter suporta enumeração (capacidade opcional). */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
|
+
import { pickModelAdapterClass } from '../adapters/factory.js';
|
|
2
3
|
/**
|
|
3
|
-
* Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
|
|
5
|
+
* Lê/escreve via `pickModelAdapterClass` (a MESMA regra do provider): com
|
|
6
|
+
* `session:` configurado, Session/Grant/tokens saem do adapter da sessão, não
|
|
7
|
+
* do default. Encapsula:
|
|
6
8
|
* - a enumeração via a capacidade opcional `list` do adapter (degrada quando
|
|
7
9
|
* ausente, igual ao CRUD de clients);
|
|
8
10
|
* - a destruição das sessões + grants da conta. Destruir um grant CASCATEIA a
|
|
@@ -16,16 +18,21 @@ import { getBootedApp } from '../../services/booted_app.js';
|
|
|
16
18
|
const GLOBAL_SESSION_LIMIT = 500;
|
|
17
19
|
export class AdminSessionsService {
|
|
18
20
|
#AdapterClass;
|
|
21
|
+
#SessionAdapterClass;
|
|
19
22
|
#accountStore;
|
|
20
23
|
/** Conexão Lucid das tabelas authkit (schema `auth`) — onde vive auth_session_revocations. */
|
|
21
24
|
#schemaConnection;
|
|
22
25
|
constructor(oidc) {
|
|
23
26
|
this.#AdapterClass = oidc.config.AdapterClass;
|
|
27
|
+
// `Session`/`Grant`/tokens vivem no adapter da sessão quando `session:`
|
|
28
|
+
// está configurado (default: o mesmo do `AdapterClass`). `??` cobre
|
|
29
|
+
// configs resolvidas por versões antigas do `defineConfig`.
|
|
30
|
+
this.#SessionAdapterClass = oidc.config.SessionAdapterClass ?? oidc.config.AdapterClass;
|
|
24
31
|
this.#accountStore = oidc.config.accountStore;
|
|
25
32
|
this.#schemaConnection = oidc.config.schema?.connection;
|
|
26
33
|
}
|
|
27
34
|
#adapter(model) {
|
|
28
|
-
return new this.#AdapterClass(model);
|
|
35
|
+
return new (pickModelAdapterClass(model, this.#AdapterClass, this.#SessionAdapterClass))(model);
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Grava uma revogação por `sub` na tabela compartilhada `auth_session_revocations`,
|
|
@@ -69,6 +69,29 @@ export interface TokenExchangeResult {
|
|
|
69
69
|
/** `act.sub` (ator provado pelo IdP), quando o IdP informa. */
|
|
70
70
|
actSub?: string;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Códigos de falha do `startImpersonation` — o controller mapeia cada um numa
|
|
74
|
+
* mensagem amigável (sem vazar segredo), em vez de um genérico único que
|
|
75
|
+
* obriga adivinhar entre "token expirado", "sem refresh" e "IdP recusou".
|
|
76
|
+
*/
|
|
77
|
+
export type ImpersonationStartErrorCode = 'already_active' | 'invalid_max_age' | 'no_admin_access_token' | 'no_account_session' | 'no_refresh_token' | 'refresh_rejected' | 'exchange_rejected';
|
|
78
|
+
/**
|
|
79
|
+
* Erro do `startImpersonation` com `code` machine-readable. `status` é o HTTP
|
|
80
|
+
* do token/refresh endpoint; `idpError` é o campo `error` do corpo
|
|
81
|
+
* (`invalid_grant`, `invalid_request`, …) — um código de allowlist, NUNCA
|
|
82
|
+
* tokens nem `error_description` (pode ecoar segredos). `cause` encadeia o
|
|
83
|
+
* erro original (ex.: o 400 do exchange que motivou a tentativa de refresh).
|
|
84
|
+
*/
|
|
85
|
+
export declare class ImpersonationStartError extends Error {
|
|
86
|
+
readonly code: ImpersonationStartErrorCode;
|
|
87
|
+
readonly status?: number;
|
|
88
|
+
readonly idpError?: string;
|
|
89
|
+
constructor(code: ImpersonationStartErrorCode, message: string, options?: {
|
|
90
|
+
status?: number;
|
|
91
|
+
idpError?: string;
|
|
92
|
+
cause?: unknown;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
72
95
|
/**
|
|
73
96
|
* Renova o access token do admin via refresh grant (RFC 6749 §6), usando o
|
|
74
97
|
* refresh token guardado por `rememberRefreshToken`. Retorna o novo access
|
|
@@ -101,6 +124,12 @@ export declare function refreshAccessToken(ctx: HttpContext, params: Pick<StartI
|
|
|
101
124
|
* encerrar continua explícito via `stopImpersonation`.
|
|
102
125
|
* - Recusa (lança) se não houver access token do admin na sessão.
|
|
103
126
|
* - Recusa (lança) `maxAge` não-positivo (fail-fast de misconfiguração).
|
|
127
|
+
*
|
|
128
|
+
* Toda falha é um `ImpersonationStartError` com `code` (`already_active`,
|
|
129
|
+
* `invalid_max_age`, `no_admin_access_token`, `no_account_session`,
|
|
130
|
+
* `no_refresh_token`, `refresh_rejected`, `exchange_rejected`) — o controller
|
|
131
|
+
* traduz cada um numa mensagem amigável. `status`/`idpError` carregam o HTTP
|
|
132
|
+
* e o `error` do IdP (códigos seguros, sem segredos) para o log server-side.
|
|
104
133
|
*/
|
|
105
134
|
export declare function startImpersonation(ctx: HttpContext, params: StartImpersonationParams): Promise<ImpersonationState>;
|
|
106
135
|
/**
|
|
@@ -64,6 +64,42 @@ export function rememberAccessToken(ctx, accessToken) {
|
|
|
64
64
|
export function rememberRefreshToken(ctx, refreshToken) {
|
|
65
65
|
ctx.session.put(ADMIN_REFRESH_TOKEN_SESSION_KEY, refreshToken);
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Erro do `startImpersonation` com `code` machine-readable. `status` é o HTTP
|
|
69
|
+
* do token/refresh endpoint; `idpError` é o campo `error` do corpo
|
|
70
|
+
* (`invalid_grant`, `invalid_request`, …) — um código de allowlist, NUNCA
|
|
71
|
+
* tokens nem `error_description` (pode ecoar segredos). `cause` encadeia o
|
|
72
|
+
* erro original (ex.: o 400 do exchange que motivou a tentativa de refresh).
|
|
73
|
+
*/
|
|
74
|
+
export class ImpersonationStartError extends Error {
|
|
75
|
+
code;
|
|
76
|
+
status;
|
|
77
|
+
idpError;
|
|
78
|
+
constructor(code, message, options) {
|
|
79
|
+
super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
80
|
+
this.name = 'ImpersonationStartError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
if (options?.status !== undefined)
|
|
83
|
+
this.status = options.status;
|
|
84
|
+
if (options?.idpError !== undefined)
|
|
85
|
+
this.idpError = options.idpError;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Extrai o campo `error` do corpo de uma resposta de erro do token endpoint
|
|
90
|
+
* (`invalid_grant`, …). Tolerante: corpo ausente/fora de JSON/sem `error`
|
|
91
|
+
* string ⇒ `undefined`. Só o código — nunca `error_description` (pode ecoar
|
|
92
|
+
* segredos) nem o corpo cru.
|
|
93
|
+
*/
|
|
94
|
+
async function readIdpErrorCode(res) {
|
|
95
|
+
try {
|
|
96
|
+
const data = (await res.json());
|
|
97
|
+
return typeof data?.error === 'string' && data.error.length > 0 ? data.error : undefined;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
67
103
|
/**
|
|
68
104
|
* POST inline do RFC 8693 token-exchange. Inline (em vez de depender de
|
|
69
105
|
* `@adonis-agora/authkit-client`) porque o client NÃO é dependência do server e
|
|
@@ -97,8 +133,10 @@ async function requestTokenExchange(params, subjectToken) {
|
|
|
97
133
|
body: body.toString(),
|
|
98
134
|
});
|
|
99
135
|
if (!res.ok) {
|
|
100
|
-
// NUNCA logamos tokens nem o corpo (pode ecoar segredos). Só o status
|
|
101
|
-
|
|
136
|
+
// NUNCA logamos tokens nem o corpo (pode ecoar segredos). Só o status +
|
|
137
|
+
// o código `error` do IdP (allowlist como `invalid_grant`).
|
|
138
|
+
const idpError = await readIdpErrorCode(res);
|
|
139
|
+
throw new ImpersonationStartError('exchange_rejected', `Token exchange failed: ${res.status}${idpError ? ` (${idpError})` : ''}`, { status: res.status, idpError });
|
|
102
140
|
}
|
|
103
141
|
const result = {};
|
|
104
142
|
try {
|
|
@@ -171,23 +209,29 @@ export async function refreshAccessToken(ctx, params) {
|
|
|
171
209
|
* encerrar continua explícito via `stopImpersonation`.
|
|
172
210
|
* - Recusa (lança) se não houver access token do admin na sessão.
|
|
173
211
|
* - Recusa (lança) `maxAge` não-positivo (fail-fast de misconfiguração).
|
|
212
|
+
*
|
|
213
|
+
* Toda falha é um `ImpersonationStartError` com `code` (`already_active`,
|
|
214
|
+
* `invalid_max_age`, `no_admin_access_token`, `no_account_session`,
|
|
215
|
+
* `no_refresh_token`, `refresh_rejected`, `exchange_rejected`) — o controller
|
|
216
|
+
* traduz cada um numa mensagem amigável. `status`/`idpError` carregam o HTTP
|
|
217
|
+
* e o `error` do IdP (códigos seguros, sem segredos) para o log server-side.
|
|
174
218
|
*/
|
|
175
219
|
export async function startImpersonation(ctx, params) {
|
|
176
220
|
if (ctx.session.get(IMPERSONATOR_SESSION_KEY)) {
|
|
177
|
-
throw new
|
|
221
|
+
throw new ImpersonationStartError('already_active', 'Impersonation already active; stop the current one before starting another');
|
|
178
222
|
}
|
|
179
223
|
if (params.maxAge !== undefined && !(params.maxAge > 0)) {
|
|
180
|
-
throw new
|
|
224
|
+
throw new ImpersonationStartError('invalid_max_age', 'Invalid maxAge: must be a positive number of seconds');
|
|
181
225
|
}
|
|
182
226
|
const adminAccessToken = ctx.session.get(ADMIN_ACCESS_TOKEN_SESSION_KEY);
|
|
183
227
|
if (!adminAccessToken) {
|
|
184
|
-
throw new
|
|
228
|
+
throw new ImpersonationStartError('no_admin_access_token', 'No admin access token in session; call rememberAccessToken after login');
|
|
185
229
|
}
|
|
186
230
|
const impersonatorId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
187
231
|
if (!impersonatorId) {
|
|
188
232
|
// Não há admin logado para impersonar como — sem identidade para restaurar
|
|
189
233
|
// depois. Recusa antes de qualquer chamada/mutação.
|
|
190
|
-
throw new
|
|
234
|
+
throw new ImpersonationStartError('no_account_session', 'No account session; log in as the admin before impersonating');
|
|
191
235
|
}
|
|
192
236
|
// O IdP é o gatekeeper: lança se o admin não puder personificar. Chamado ANTES
|
|
193
237
|
// de qualquer mutação de sessão — em caso de erro nada é trocado.
|
|
@@ -197,12 +241,26 @@ export async function startImpersonation(ctx, params) {
|
|
|
197
241
|
}
|
|
198
242
|
catch (err) {
|
|
199
243
|
// Access token expirado (4xx do exchange): renova via refresh token e tenta
|
|
200
|
-
// de novo. Sem refresh token / refresh falho →
|
|
201
|
-
|
|
244
|
+
// de novo. Sem refresh token / refresh falho → erro TIPADO (o controller
|
|
245
|
+
// distingue "entre de novo" de "IdP recusou"), com o 4xx original em `cause`.
|
|
246
|
+
const status = err instanceof ImpersonationStartError ? err.status : undefined;
|
|
247
|
+
const isExpiredToken = err instanceof Error && status !== undefined && status >= 400 && status < 500;
|
|
248
|
+
if (!isExpiredToken)
|
|
202
249
|
throw err;
|
|
250
|
+
const hasRefreshToken = Boolean(ctx.session.get(ADMIN_REFRESH_TOKEN_SESSION_KEY));
|
|
251
|
+
if (!hasRefreshToken) {
|
|
252
|
+
throw new ImpersonationStartError('no_refresh_token', `Admin access token expired (Token exchange failed: ${status}${err instanceof ImpersonationStartError && err.idpError ? ` (${err.idpError})` : ''}) and no refresh token in session; log in again`, {
|
|
253
|
+
status,
|
|
254
|
+
...(err instanceof ImpersonationStartError && err.idpError
|
|
255
|
+
? { idpError: err.idpError }
|
|
256
|
+
: {}),
|
|
257
|
+
cause: err,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
203
260
|
const refreshed = await refreshAccessToken(ctx, params);
|
|
204
|
-
if (!refreshed)
|
|
205
|
-
throw err;
|
|
261
|
+
if (!refreshed) {
|
|
262
|
+
throw new ImpersonationStartError('refresh_rejected', 'Admin access token expired and refresh token was rejected; log in again', { status, cause: err });
|
|
263
|
+
}
|
|
206
264
|
rememberAccessToken(ctx, refreshed.accessToken);
|
|
207
265
|
if (refreshed.refreshToken)
|
|
208
266
|
rememberRefreshToken(ctx, refreshed.refreshToken);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as oidc from 'oidc-provider';
|
|
2
|
+
import { pickModelAdapterClass } from '../adapters/factory.js';
|
|
2
3
|
import { createDeviceSources } from './device_sources.js';
|
|
3
4
|
import { createLogoutSources } from './logout_sources.js';
|
|
4
5
|
/** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
|
|
@@ -92,7 +93,12 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
|
|
|
92
93
|
}
|
|
93
94
|
: {};
|
|
94
95
|
const provider = new oidc.Provider(config.issuer, {
|
|
95
|
-
|
|
96
|
+
// Dispatcher por modelo (suportado pelo oidc-provider: `Adapter` aceita
|
|
97
|
+
// função `(name) => adapter` além de classe). Session-scoped vai pro
|
|
98
|
+
// `SessionAdapterClass`, o resto pro `AdapterClass` — mesma regra de
|
|
99
|
+
// `pickModelAdapterClass`, usada também pelos serviços que instanciam
|
|
100
|
+
// adapter na mão. Sem `session.adapter`, as duas classes são a mesma.
|
|
101
|
+
adapter: ((name) => new (pickModelAdapterClass(name, config.AdapterClass, config.SessionAdapterClass))(name)),
|
|
96
102
|
clients: config.clients.map((c) => ({
|
|
97
103
|
client_id: c.clientId,
|
|
98
104
|
client_secret: c.clientSecret,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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",
|