@adonis-agora/authkit-client 0.15.0 → 0.17.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
@@ -16,6 +16,8 @@ export { default as BackchannelRevocationMiddleware } from './src/middleware/bac
16
16
  export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
17
17
  export type { UnauthorizedAccessConstructor } from './src/host/authkit_client_guard.js';
18
18
  export { getAuthkit } from './src/host/request_authenticator.js';
19
+ export { isAuthkitAdmin, authkitDashboardAuthorize, authkitDashboardMiddleware, } from './src/host/dashboard_guard.js';
20
+ export type { DashboardGuardOptions } from './src/host/dashboard_guard.js';
19
21
  export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
20
22
  export { lucidMirror } from './src/lucid_mirror.js';
21
23
  export type { LucidMirrorOptions } from './src/lucid_mirror.js';
package/build/index.js CHANGED
@@ -9,6 +9,7 @@ export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocatio
9
9
  export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
10
10
  export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
11
11
  export { getAuthkit } from './src/host/request_authenticator.js';
12
+ export { isAuthkitAdmin, authkitDashboardAuthorize, authkitDashboardMiddleware, } from './src/host/dashboard_guard.js';
12
13
  export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
13
14
  export { lucidMirror } from './src/lucid_mirror.js';
14
15
  export { registerOidcClient } from './src/register_oidc_client.js';
@@ -2,6 +2,7 @@ import type { HttpContext } from '@adonisjs/core/http';
2
2
  import type { ApplicationService } from '@adonisjs/core/types';
3
3
  import { Authenticator } from '../src/authenticator.js';
4
4
  import type { ResolvedClientConfig } from '../src/define_config.js';
5
+ import type { TokenSet } from '../src/types.js';
5
6
  /** Deps injetáveis (relógio/fetch) para testar `maybeRefresh` sem rede/tempo real. */
6
7
  export interface RefreshDeps {
7
8
  now?: () => number;
@@ -23,6 +24,9 @@ export declare class AuthkitClientManager {
23
24
  * renovado — incluindo o refresh_token ROTACIONADO — de volta na sessão. Preserva
24
25
  * o id_token / refresh_token anteriores quando o IdP não os reemite.
25
26
  *
27
+ * Preserva TAMBÉM o `impersonationBinding`: renovar o token do alvo não muda a
28
+ * sessão, então não pode invalidar o vínculo e trancar o ator na impersonação.
29
+ *
26
30
  * Best-effort: qualquer falha (token revogado, IdP fora) é silenciosa — o resolver
27
31
  * lida com a sessão expirada no fluxo normal. Chamado pelo middleware por request.
28
32
  */
@@ -43,10 +47,64 @@ export declare class AuthkitClientManager {
43
47
  * do chamador (ex.: Bouncer) ANTES de chamar este método.
44
48
  */
45
49
  impersonate(ctx: HttpContext, requestedSubject: string): Promise<void>;
46
- /** Encerra a impersonação restaurando o token set original. Retorna false se não havia impersonação. */
50
+ /**
51
+ * Encerra a impersonação restaurando o token set do ator — SE a sessão que pede
52
+ * ainda for aquela para a qual a credencial foi guardada.
53
+ *
54
+ * A credencial parqueada é um refresh token vivo; sem essa conferência o restore
55
+ * a entrega a quem estiver de posse do cookie jar depois (logout do ator + login
56
+ * de outra identidade). A prova é o `impersonationBinding`: um nonce escrito ao
57
+ * mesmo tempo dentro do token set ATIVO e do registro parqueado. Só `impersonate()`
58
+ * o escreve e só o `maybeRefresh` o propaga — qualquer troca de identidade
59
+ * reescreve o slot da sessão e o vínculo some.
60
+ *
61
+ * As conferências ÓBVIAS não servem, e é por isso que o vínculo existe:
62
+ * - `sub === sub do ator`: durante uma impersonação legítima a sessão é a do
63
+ * ALVO, então isso nunca é verdade — trancaria o ator dentro da impersonação;
64
+ * - "o token parqueado é de um admin?": sempre verdade; o atacante não alega
65
+ * ser o ator, ele pede a credencial do ator;
66
+ * - gate por papel: o ator impersonando carrega os papéis do ALVO nesse momento.
67
+ *
68
+ * Recusa é FAIL-CLOSED: a credencial órfã é DESCARTADA (nunca devolvida) e o
69
+ * token set da sessão também cai — uma recusa não é uma tentativa a repetir, e
70
+ * ninguém fica preso impersonando.
71
+ *
72
+ * LIMITE HONESTO: quem controla o cookie jar DURANTE uma impersonação ativa é
73
+ * indistinguível do ator — é literalmente a mesma sessão, e o vínculo viaja nela.
74
+ * Isso é o modelo de sessão, não esta conferência. O que fecha é a janela em que
75
+ * a credencial sobrevive à sessão que a guardou.
76
+ *
77
+ * @returns `true` só quando o ator foi restaurado.
78
+ */
47
79
  stopImpersonating(ctx: HttpContext): Promise<boolean>;
48
- /** Indica se a request corrente está impersonando (p/ exibir banner na UI). */
80
+ /**
81
+ * Indica se a request corrente está impersonando (p/ exibir banner na UI). Exige o
82
+ * mesmo vínculo do `stopImpersonating` — uma credencial órfã de outra sessão não
83
+ * faz o banner aparecer para quem não está impersonando ninguém.
84
+ */
49
85
  isImpersonating(ctx: HttpContext): boolean;
86
+ /**
87
+ * Encerra a sessão do AuthKit: remove o token set E qualquer credencial de
88
+ * impersonação parqueada. Todo caminho da lib que derruba a sessão passa por aqui
89
+ * — logout RP-initiated (`registerOidcClient`) e revogação back-channel.
90
+ *
91
+ * Existe porque um `session.forget(sessionKey)` solto deixava o refresh token do
92
+ * ATOR parqueado no browser depois de uma revogação central — o mecanismo que o
93
+ * operador usa justamente para matar a sessão.
94
+ */
95
+ endSession(ctx: HttpContext): void;
96
+ /**
97
+ * Instala o token set de um login recém-concluído, descartando o que sobrou da
98
+ * sessão anterior no mesmo cookie jar.
99
+ *
100
+ * O `stopImpersonating` já recusa uma credencial órfã — o token set novo nasce sem
101
+ * `impersonationBinding`, então o vínculo não fecha. Mas recusar é só não DEVOLVER:
102
+ * sem esta limpeza o refresh token do ator continua serializado no cookie de sessão
103
+ * de outra identidade até a sessão expirar. Não devolver e não guardar são coisas
104
+ * diferentes, e a segunda é a que mantém a invariante — a credencial parqueada nunca
105
+ * sobrevive à sessão que a parqueou.
106
+ */
107
+ startSession(ctx: HttpContext, tokenSet: TokenSet): void;
50
108
  createAuthenticator(ctx: HttpContext): Promise<Authenticator>;
51
109
  }
52
110
  declare module '@adonisjs/core/types' {
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { configProvider } from '@adonisjs/core';
2
3
  import { RuntimeException } from '@adonisjs/core/exceptions';
3
4
  import { Authenticator } from '../src/authenticator.js';
@@ -5,6 +6,17 @@ import { validateLogoutToken } from '../src/backchannel_logout.js';
5
6
  import { exchangeToken, refreshTokens } from '../src/oidc_login.js';
6
7
  /** Margem (ms) antes do `expiresAt` em que o access token é renovado proativamente. */
7
8
  const REFRESH_SKEW_MS = 60_000;
9
+ /** Aceita só o formato COM vínculo; o formato legado (TokenSet cru) vira `null`. */
10
+ function readParked(value) {
11
+ if (!value || typeof value !== 'object')
12
+ return null;
13
+ const candidate = value;
14
+ if (typeof candidate.binding !== 'string' || !candidate.binding)
15
+ return null;
16
+ if (!candidate.tokenSet || typeof candidate.tokenSet !== 'object')
17
+ return null;
18
+ return candidate;
19
+ }
8
20
  export class AuthkitClientManager {
9
21
  config;
10
22
  #resolverCache;
@@ -40,6 +52,9 @@ export class AuthkitClientManager {
40
52
  * renovado — incluindo o refresh_token ROTACIONADO — de volta na sessão. Preserva
41
53
  * o id_token / refresh_token anteriores quando o IdP não os reemite.
42
54
  *
55
+ * Preserva TAMBÉM o `impersonationBinding`: renovar o token do alvo não muda a
56
+ * sessão, então não pode invalidar o vínculo e trancar o ator na impersonação.
57
+ *
43
58
  * Best-effort: qualquer falha (token revogado, IdP fora) é silenciosa — o resolver
44
59
  * lida com a sessão expirada no fluxo normal. Chamado pelo middleware por request.
45
60
  */
@@ -68,6 +83,7 @@ export class AuthkitClientManager {
68
83
  accessToken: next.accessToken,
69
84
  refreshToken: next.refreshToken ?? tokenSet.refreshToken,
70
85
  expiresAt: next.expiresAt,
86
+ impersonationBinding: tokenSet.impersonationBinding,
71
87
  });
72
88
  }
73
89
  catch {
@@ -140,24 +156,113 @@ export class AuthkitClientManager {
140
156
  requestedSubject,
141
157
  resilience: this.config.resilience,
142
158
  });
143
- session.put(this.#impersonatorKey, current);
144
- session.put(this.config.sessionKey, impersonated);
159
+ // Vínculo: o mesmo nonce nos dois lados. O lado ATIVO mora dentro do token set
160
+ // da sessão, o único slot que todo caminho de fim-de-sessão já limpa.
161
+ const binding = randomUUID();
162
+ session.put(this.#impersonatorKey, {
163
+ tokenSet: current,
164
+ binding,
165
+ });
166
+ session.put(this.config.sessionKey, { ...impersonated, impersonationBinding: binding });
145
167
  }
146
- /** Encerra a impersonação restaurando o token set original. Retorna false se não havia impersonação. */
168
+ /**
169
+ * Encerra a impersonação restaurando o token set do ator — SE a sessão que pede
170
+ * ainda for aquela para a qual a credencial foi guardada.
171
+ *
172
+ * A credencial parqueada é um refresh token vivo; sem essa conferência o restore
173
+ * a entrega a quem estiver de posse do cookie jar depois (logout do ator + login
174
+ * de outra identidade). A prova é o `impersonationBinding`: um nonce escrito ao
175
+ * mesmo tempo dentro do token set ATIVO e do registro parqueado. Só `impersonate()`
176
+ * o escreve e só o `maybeRefresh` o propaga — qualquer troca de identidade
177
+ * reescreve o slot da sessão e o vínculo some.
178
+ *
179
+ * As conferências ÓBVIAS não servem, e é por isso que o vínculo existe:
180
+ * - `sub === sub do ator`: durante uma impersonação legítima a sessão é a do
181
+ * ALVO, então isso nunca é verdade — trancaria o ator dentro da impersonação;
182
+ * - "o token parqueado é de um admin?": sempre verdade; o atacante não alega
183
+ * ser o ator, ele pede a credencial do ator;
184
+ * - gate por papel: o ator impersonando carrega os papéis do ALVO nesse momento.
185
+ *
186
+ * Recusa é FAIL-CLOSED: a credencial órfã é DESCARTADA (nunca devolvida) e o
187
+ * token set da sessão também cai — uma recusa não é uma tentativa a repetir, e
188
+ * ninguém fica preso impersonando.
189
+ *
190
+ * LIMITE HONESTO: quem controla o cookie jar DURANTE uma impersonação ativa é
191
+ * indistinguível do ator — é literalmente a mesma sessão, e o vínculo viaja nela.
192
+ * Isso é o modelo de sessão, não esta conferência. O que fecha é a janela em que
193
+ * a credencial sobrevive à sessão que a guardou.
194
+ *
195
+ * @returns `true` só quando o ator foi restaurado.
196
+ */
147
197
  async stopImpersonating(ctx) {
148
198
  const session = ctx.session;
149
199
  if (!session)
150
200
  return false;
151
- const original = session.get(this.#impersonatorKey);
152
- if (!original)
201
+ const raw = session.get(this.#impersonatorKey);
202
+ if (raw === undefined || raw === null)
203
+ return false;
204
+ const parked = readParked(raw);
205
+ const live = session.get(this.config.sessionKey);
206
+ // `parked === null` cobre o formato pré-0.15 (TokenSet cru, sem vínculo nenhum):
207
+ // é justamente a credencial que não dá pra atribuir. Recusada como qualquer outra.
208
+ if (!parked || !live?.impersonationBinding || live.impersonationBinding !== parked.binding) {
209
+ session.forget(this.#impersonatorKey);
210
+ session.forget(this.config.sessionKey);
153
211
  return false;
212
+ }
213
+ const { impersonationBinding: _binding, ...original } = parked.tokenSet;
154
214
  session.put(this.config.sessionKey, original);
155
215
  session.forget(this.#impersonatorKey);
156
216
  return true;
157
217
  }
158
- /** Indica se a request corrente está impersonando (p/ exibir banner na UI). */
218
+ /**
219
+ * Indica se a request corrente está impersonando (p/ exibir banner na UI). Exige o
220
+ * mesmo vínculo do `stopImpersonating` — uma credencial órfã de outra sessão não
221
+ * faz o banner aparecer para quem não está impersonando ninguém.
222
+ */
159
223
  isImpersonating(ctx) {
160
- return Boolean(ctx.session?.get(this.#impersonatorKey));
224
+ const session = ctx.session;
225
+ if (!session)
226
+ return false;
227
+ const parked = readParked(session.get(this.#impersonatorKey));
228
+ if (!parked)
229
+ return false;
230
+ const live = session.get(this.config.sessionKey);
231
+ return live?.impersonationBinding === parked.binding;
232
+ }
233
+ /**
234
+ * Encerra a sessão do AuthKit: remove o token set E qualquer credencial de
235
+ * impersonação parqueada. Todo caminho da lib que derruba a sessão passa por aqui
236
+ * — logout RP-initiated (`registerOidcClient`) e revogação back-channel.
237
+ *
238
+ * Existe porque um `session.forget(sessionKey)` solto deixava o refresh token do
239
+ * ATOR parqueado no browser depois de uma revogação central — o mecanismo que o
240
+ * operador usa justamente para matar a sessão.
241
+ */
242
+ endSession(ctx) {
243
+ const session = ctx.session;
244
+ if (!session)
245
+ return;
246
+ session.forget(this.config.sessionKey);
247
+ session.forget(this.#impersonatorKey);
248
+ }
249
+ /**
250
+ * Instala o token set de um login recém-concluído, descartando o que sobrou da
251
+ * sessão anterior no mesmo cookie jar.
252
+ *
253
+ * O `stopImpersonating` já recusa uma credencial órfã — o token set novo nasce sem
254
+ * `impersonationBinding`, então o vínculo não fecha. Mas recusar é só não DEVOLVER:
255
+ * sem esta limpeza o refresh token do ator continua serializado no cookie de sessão
256
+ * de outra identidade até a sessão expirar. Não devolver e não guardar são coisas
257
+ * diferentes, e a segunda é a que mantém a invariante — a credencial parqueada nunca
258
+ * sobrevive à sessão que a parqueou.
259
+ */
260
+ startSession(ctx, tokenSet) {
261
+ const session = ctx.session;
262
+ if (!session)
263
+ return;
264
+ session.forget(this.#impersonatorKey);
265
+ session.put(this.config.sessionKey, tokenSet);
161
266
  }
162
267
  async createAuthenticator(ctx) {
163
268
  const resolver = await this.#getResolver();
@@ -0,0 +1,52 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ export interface DashboardGuardOptions {
3
+ /** Global role required to pass the gate. Default `'ADMIN'`. */
4
+ role?: string;
5
+ }
6
+ /**
7
+ * Core predicate behind {@link authkitDashboardAuthorize} and
8
+ * {@link authkitDashboardMiddleware}: authenticated (a resolvable authkit `Identity`) AND holding
9
+ * the configured global role — `ctx.auth.hasGlobalRole('ADMIN')`, the pattern hosts already
10
+ * hand-roll per embedded admin dashboard (durable/media/agent/telescope).
11
+ *
12
+ * Resolves the session via {@link getAuthkit} rather than reading `ctx.auth` directly: `ctx.auth`'s
13
+ * TYPE is host-owned (each app augments `HttpContext.auth` for its own user model, or leaves it to
14
+ * `@adonisjs/auth`'s guard), so this module — shipped from the library, with no visibility into
15
+ * that augmentation — can't assume its shape. `getAuthkit` is the one session-resolution path that
16
+ * works either way (legacy `authkit_middleware`'s `ctx.auth` or the newer `authkitClientGuard`'s
17
+ * `ctx.authkit`), memoizing so a second call in the same request is free.
18
+ */
19
+ export declare function isAuthkitAdmin(ctx: HttpContext, options?: DashboardGuardOptions): Promise<boolean>;
20
+ /**
21
+ * Adapter for the `authorize: (ctx) => boolean` shape that `@adonis-agora/durable`,
22
+ * `@adonis-agora/agent` and `@adonis-agora/telescope` dashboard configs expect. A `false` return
23
+ * makes the provider reply `403 { error: 'forbidden' }` — the provider's own behavior, unchanged
24
+ * here.
25
+ *
26
+ * ```ts
27
+ * // config/durable_dashboard.ts
28
+ * import { defineConfig } from '@adonis-agora/durable/dashboard'
29
+ * import { authkitDashboardAuthorize } from '@adonis-agora/authkit-client'
30
+ *
31
+ * export default defineConfig({
32
+ * authorize: authkitDashboardAuthorize(),
33
+ * })
34
+ * ```
35
+ */
36
+ export declare function authkitDashboardAuthorize(options?: DashboardGuardOptions): (ctx: HttpContext) => Promise<boolean>;
37
+ /**
38
+ * Adapter for the `middleware: (ctx, next) => Promise<void>` shape that
39
+ * `@adonis-agora/media`'s dashboard config expects. Mirrors the hand-rolled `requireAdmin` apps
40
+ * wrote themselves: no session -> `401`; session without the role -> `403`; otherwise `next()`.
41
+ *
42
+ * ```ts
43
+ * // config/media_dashboard.ts
44
+ * import { defineConfig } from '@adonis-agora/media/dashboard'
45
+ * import { authkitDashboardMiddleware } from '@adonis-agora/authkit-client'
46
+ *
47
+ * export default defineConfig({
48
+ * middleware: authkitDashboardMiddleware(),
49
+ * })
50
+ * ```
51
+ */
52
+ export declare function authkitDashboardMiddleware(options?: DashboardGuardOptions): (ctx: HttpContext, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { getAuthkit } from './request_authenticator.js';
2
+ /** Global role that counts as "admin" when the caller doesn't override it. Mirrors the default
3
+ * used across `@adonis-agora/authkit-server`'s own admin console (`admin.roles`, `adminRoles` on
4
+ * impersonation/token-exchange) — same convention, not a new one. */
5
+ const DEFAULT_ADMIN_ROLE = 'ADMIN';
6
+ /**
7
+ * Core predicate behind {@link authkitDashboardAuthorize} and
8
+ * {@link authkitDashboardMiddleware}: authenticated (a resolvable authkit `Identity`) AND holding
9
+ * the configured global role — `ctx.auth.hasGlobalRole('ADMIN')`, the pattern hosts already
10
+ * hand-roll per embedded admin dashboard (durable/media/agent/telescope).
11
+ *
12
+ * Resolves the session via {@link getAuthkit} rather than reading `ctx.auth` directly: `ctx.auth`'s
13
+ * TYPE is host-owned (each app augments `HttpContext.auth` for its own user model, or leaves it to
14
+ * `@adonisjs/auth`'s guard), so this module — shipped from the library, with no visibility into
15
+ * that augmentation — can't assume its shape. `getAuthkit` is the one session-resolution path that
16
+ * works either way (legacy `authkit_middleware`'s `ctx.auth` or the newer `authkitClientGuard`'s
17
+ * `ctx.authkit`), memoizing so a second call in the same request is free.
18
+ */
19
+ export async function isAuthkitAdmin(ctx, options = {}) {
20
+ const authkit = await getAuthkit(ctx);
21
+ if (!(await authkit.check()))
22
+ return false;
23
+ return authkit.hasGlobalRole(options.role ?? DEFAULT_ADMIN_ROLE);
24
+ }
25
+ /**
26
+ * Adapter for the `authorize: (ctx) => boolean` shape that `@adonis-agora/durable`,
27
+ * `@adonis-agora/agent` and `@adonis-agora/telescope` dashboard configs expect. A `false` return
28
+ * makes the provider reply `403 { error: 'forbidden' }` — the provider's own behavior, unchanged
29
+ * here.
30
+ *
31
+ * ```ts
32
+ * // config/durable_dashboard.ts
33
+ * import { defineConfig } from '@adonis-agora/durable/dashboard'
34
+ * import { authkitDashboardAuthorize } from '@adonis-agora/authkit-client'
35
+ *
36
+ * export default defineConfig({
37
+ * authorize: authkitDashboardAuthorize(),
38
+ * })
39
+ * ```
40
+ */
41
+ export function authkitDashboardAuthorize(options = {}) {
42
+ return (ctx) => isAuthkitAdmin(ctx, options);
43
+ }
44
+ /**
45
+ * Adapter for the `middleware: (ctx, next) => Promise<void>` shape that
46
+ * `@adonis-agora/media`'s dashboard config expects. Mirrors the hand-rolled `requireAdmin` apps
47
+ * wrote themselves: no session -> `401`; session without the role -> `403`; otherwise `next()`.
48
+ *
49
+ * ```ts
50
+ * // config/media_dashboard.ts
51
+ * import { defineConfig } from '@adonis-agora/media/dashboard'
52
+ * import { authkitDashboardMiddleware } from '@adonis-agora/authkit-client'
53
+ *
54
+ * export default defineConfig({
55
+ * middleware: authkitDashboardMiddleware(),
56
+ * })
57
+ * ```
58
+ */
59
+ export function authkitDashboardMiddleware(options = {}) {
60
+ return async (ctx, next) => {
61
+ const authkit = await getAuthkit(ctx);
62
+ if (!(await authkit.check())) {
63
+ ctx.response.unauthorized({ message: 'Authentication required' });
64
+ return;
65
+ }
66
+ if (!authkit.hasGlobalRole(options.role ?? DEFAULT_ADMIN_ROLE)) {
67
+ ctx.response.forbidden({ message: 'Admin access required' });
68
+ return;
69
+ }
70
+ await next();
71
+ };
72
+ }
@@ -17,7 +17,6 @@ export default class BackchannelRevocationMiddleware {
17
17
  const store = manager.clientConfig.backchannelStore;
18
18
  if (!store)
19
19
  return next();
20
- const sessionKey = manager.clientConfig.sessionKey;
21
20
  const idToken = manager.getIdToken(ctx);
22
21
  if (idToken) {
23
22
  const claims = decodeJwtPayload(idToken);
@@ -25,8 +24,9 @@ export default class BackchannelRevocationMiddleware {
25
24
  const sub = typeof claims?.sub === 'string' ? claims.sub : undefined;
26
25
  const authTime = typeof claims?.iat === 'number' ? claims.iat : undefined;
27
26
  if ((sid || sub) && (await store.isRevoked({ sid, sub, authTime }))) {
28
- // session é augmentado por @adonisjs/session no app host; na lib usamos cast.
29
- ctx.session?.forget(sessionKey);
27
+ // endSession, não `forget(sessionKey)`: uma revogação central não pode deixar
28
+ // a credencial de impersonação (refresh token do ATOR) parqueada no browser.
29
+ manager.endSession(ctx);
30
30
  }
31
31
  }
32
32
  return next();
@@ -63,7 +63,9 @@ export function registerOidcClient(router, options = {}) {
63
63
  code,
64
64
  codeVerifier: pkce.verifier,
65
65
  });
66
- ctx.session?.put(cfg.sessionKey, tokenSet);
66
+ // startSession, não `put(sessionKey)`: um login novo no mesmo cookie jar não pode
67
+ // herdar a credencial de impersonação parqueada pela sessão anterior.
68
+ manager.startSession(ctx, tokenSet);
67
69
  ctx.session?.forget(pkceKey);
68
70
  const authenticator = await manager.createAuthenticator(ctx);
69
71
  const identity = await authenticator.getIdentity();
@@ -81,7 +83,9 @@ export function registerOidcClient(router, options = {}) {
81
83
  const manager = await ctx.containerResolver.make('authkit.client');
82
84
  const cfg = manager.clientConfig;
83
85
  const idToken = manager.getIdToken(ctx);
84
- ctx.session?.forget(cfg.sessionKey);
86
+ // Leva junto a credencial de impersonação parqueada — era este logout que a
87
+ // deixava para trás, viva, para a próxima identidade do mesmo cookie jar.
88
+ manager.endSession(ctx);
85
89
  const postLogoutRedirectUri = typeof options.postLogoutRedirect === 'function'
86
90
  ? options.postLogoutRedirect(ctx)
87
91
  : (options.postLogoutRedirect ?? `${new URL(cfg.redirectUri).origin}/`);
@@ -3,4 +3,13 @@ export interface TokenSet {
3
3
  accessToken: string;
4
4
  refreshToken?: string;
5
5
  expiresAt?: number;
6
+ /**
7
+ * Nonce opaco que amarra ESTE token set à credencial de impersonação parqueada
8
+ * em `${sessionKey}_impersonator`. Escrito só por `impersonate()`; preservado
9
+ * pelo `maybeRefresh`; conferido pelo `stopImpersonating()`. Qualquer outra
10
+ * escrita no slot da sessão (login novo, logout+login) nasce sem ele — que é
11
+ * exatamente o sinal de que a sessão deixou de ser aquela p/ a qual a
12
+ * credencial foi guardada. Não é segredo de autenticação: é um vínculo.
13
+ */
14
+ impersonationBinding?: string;
6
15
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-client",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 relying-party (client) toolkit: session-based authentication against an OpenID Connect identity provider, with JWT/PAT user resolvers and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
7
7
  "homepage": "https://github.com/DavideCarvalho/adonis-authkit/tree/main/packages/authkit-client#readme",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/DavideCarvalho/adonis-authkit.git",
10
+ "url": "https://github.com/DavideCarvalho/adonis-agora-authkit.git",
11
11
  "directory": "packages/authkit-client"
12
12
  },
13
13
  "bugs": {
@@ -39,6 +39,7 @@
39
39
  "./services/main": "./build/services/main.js",
40
40
  "./authkit_client_provider": "./build/providers/authkit_client_provider.js",
41
41
  "./auth": "./build/src/host/authkit_client_guard.js",
42
+ "./dashboard_guard": "./build/src/host/dashboard_guard.js",
42
43
  "./authkit_middleware": "./build/src/middleware/authkit_middleware.js",
43
44
  "./authkit_context_middleware": "./build/src/middleware/authkit_context_middleware.js",
44
45
  "./backchannel_revocation_middleware": "./build/src/middleware/backchannel_revocation_middleware.js",
@@ -74,23 +75,23 @@
74
75
  }
75
76
  },
76
77
  "dependencies": {
77
- "jose": "^5.9.0",
78
+ "jose": "5.10.0",
78
79
  "@adonis-agora/authkit-core": "0.7.0"
79
80
  },
80
81
  "devDependencies": {
81
82
  "@adonisjs/auth": "10.1.0",
82
- "@adonisjs/core": "7.3.5",
83
- "@adonis-agora/resilience": "^0.1.0",
83
+ "@adonisjs/core": "7.4.0",
84
+ "@adonis-agora/resilience": "0.3.1",
84
85
  "@adonisjs/session": "8.1.0",
85
86
  "@adonisjs/lucid": "22.4.2",
86
87
  "@adonisjs/tsconfig": "2.0.0",
87
88
  "@japa/assert": "4.2.0",
88
89
  "@japa/runner": "5.3.0",
89
90
  "@poppinss/ts-exec": "1.4.4",
90
- "ioredis-mock": "^8.9.0",
91
+ "ioredis-mock": "8.13.1",
91
92
  "@types/node": "25.9.1",
92
93
  "typescript": "6.0.3",
93
- "@adonis-agora/authkit-server": "0.55.0"
94
+ "@adonis-agora/authkit-server": "0.56.0"
94
95
  },
95
96
  "scripts": {
96
97
  "build": "tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\"",