@adonis-agora/authkit-client 0.14.0 → 0.16.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.
Files changed (37) hide show
  1. package/build/commands/configure.js +5 -3
  2. package/build/index.d.ts +32 -28
  3. package/build/index.js +18 -15
  4. package/build/providers/authkit_client_provider.d.ts +66 -8
  5. package/build/providers/authkit_client_provider.js +130 -21
  6. package/build/services/main.d.ts +1 -1
  7. package/build/services/main.js +2 -2
  8. package/build/src/authenticator.d.ts +3 -3
  9. package/build/src/authenticator.js +4 -4
  10. package/build/src/define_config.d.ts +5 -5
  11. package/build/src/define_config.js +5 -5
  12. package/build/src/discovery.d.ts +1 -1
  13. package/build/src/discovery.js +5 -5
  14. package/build/src/dpop.js +1 -1
  15. package/build/src/host/authkit_client_guard.d.ts +100 -0
  16. package/build/src/host/authkit_client_guard.js +153 -0
  17. package/build/src/host/request_authenticator.d.ts +31 -0
  18. package/build/src/host/request_authenticator.js +24 -0
  19. package/build/src/middleware/authkit_context_middleware.d.ts +28 -0
  20. package/build/src/middleware/authkit_context_middleware.js +30 -0
  21. package/build/src/middleware/backchannel_revocation_middleware.js +3 -4
  22. package/build/src/observability/context_bridge.d.ts +1 -1
  23. package/build/src/observability/context_bridge.js +8 -8
  24. package/build/src/oidc_login.d.ts +2 -2
  25. package/build/src/oidc_login.js +29 -31
  26. package/build/src/register_oidc_client.d.ts +1 -1
  27. package/build/src/register_oidc_client.js +7 -4
  28. package/build/src/resolvers/factory.js +2 -2
  29. package/build/src/resolvers/jwt_resolver.d.ts +1 -1
  30. package/build/src/resolvers/jwt_resolver.js +12 -1
  31. package/build/src/resolvers/opaque_resolver.d.ts +1 -1
  32. package/build/src/resolvers/pat_resolver.d.ts +1 -1
  33. package/build/src/revocation/revocation_store.js +0 -0
  34. package/build/src/types.d.ts +9 -0
  35. package/build/types.d.ts +1 -1
  36. package/package.json +10 -3
  37. package/stubs/main.ts +2 -2
@@ -4,13 +4,15 @@ export async function configure(command) {
4
4
  await codemods.makeUsingStub(stubsRoot, 'config/authkit_client.stub', {});
5
5
  await codemods.makeUsingStub(stubsRoot, 'controllers/oidc_session_controller.stub', {});
6
6
  await codemods.updateRcFile((rc) => rc.addProvider('@adonis-agora/authkit-client/authkit_client_provider'));
7
- await codemods.registerMiddleware('router', [{ path: '@adonis-agora/authkit-client/authkit_middleware' }]);
7
+ await codemods.registerMiddleware('router', [
8
+ { path: '@adonis-agora/authkit-client/authkit_middleware' },
9
+ ]);
8
10
  await codemods.defineEnvValidations({
9
11
  leadingComment: 'Variáveis do @adonis-agora/authkit-client (OIDC client)',
10
12
  variables: {
11
13
  AUTHKIT_ISSUER: `Env.schema.string({ format: 'url', tld: false })`,
12
- AUTHKIT_CLIENT_ID: `Env.schema.string()`,
13
- AUTHKIT_CLIENT_SECRET: `Env.schema.string.optional()`,
14
+ AUTHKIT_CLIENT_ID: 'Env.schema.string()',
15
+ AUTHKIT_CLIENT_SECRET: 'Env.schema.string.optional()',
14
16
  AUTHKIT_REDIRECT_URI: `Env.schema.string({ format: 'url', tld: false })`,
15
17
  },
16
18
  });
package/build/index.d.ts CHANGED
@@ -1,28 +1,32 @@
1
- export { defineConfig, resolvers } from "./src/define_config.js";
2
- export { Authenticator } from "./src/authenticator.js";
3
- export { identityToUser, createUserinfoResolver, } from "./src/user_resolvers.js";
4
- export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from "./src/user_resolvers.js";
5
- export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from "./src/oidc_login.js";
6
- export type { EndSessionParams, RefreshParams } from "./src/oidc_login.js";
7
- export { discoverEndpoints, conventionEndpoints } from "./src/discovery.js";
8
- export type { OidcEndpoints, DiscoverOptions } from "./src/discovery.js";
9
- export { resilientFetch } from "./src/http/resilient_fetch.js";
10
- export type { ResiliencePolicy } from "./src/http/resilient_fetch.js";
11
- export { AuthkitClientManager } from "./providers/authkit_client_provider.js";
12
- export type { ResolvedClientConfig, ClientConfigInput, BackchannelLogoutCallback, BackchannelLogoutInput, } from "./src/define_config.js";
13
- export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from "./src/revocation/revocation_store.js";
14
- export type { RevocationStore, LucidRevocationStoreOptions, } from "./src/revocation/revocation_store.js";
15
- export { default as BackchannelRevocationMiddleware } from "./src/middleware/backchannel_revocation_middleware.js";
16
- export { lucidMirror } from "./src/lucid_mirror.js";
17
- export type { LucidMirrorOptions } from "./src/lucid_mirror.js";
18
- export { registerOidcClient } from "./src/register_oidc_client.js";
19
- export type { RegisterOidcClientOptions, PostLoginRedirects, } from "./src/register_oidc_client.js";
20
- export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from "./src/backchannel_logout.js";
21
- export type { ValidateLogoutTokenOptions, ValidatedLogoutToken, SessionIndex, SessionIndexEntry, } from "./src/backchannel_logout.js";
22
- export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from "./src/dpop.js";
23
- export type { DpopKeyPair, CreateDpopProofInput } from "./src/dpop.js";
24
- export { verifyJwtAccessToken, clearJwksCache, } from "./src/verify_access_token.js";
25
- export type { VerifyJwtAccessTokenOptions, JwtAccessTokenClaims, } from "./src/verify_access_token.js";
26
- export type { TokenSet } from "./src/types.js";
27
- export type { Identity } from "@adonis-agora/authkit-core";
28
- export { configure } from "./commands/configure.js";
1
+ export { defineConfig, resolvers } from './src/define_config.js';
2
+ export { Authenticator } from './src/authenticator.js';
3
+ export { identityToUser, createUserinfoResolver, } from './src/user_resolvers.js';
4
+ export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from './src/user_resolvers.js';
5
+ export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from './src/oidc_login.js';
6
+ export type { EndSessionParams, RefreshParams } from './src/oidc_login.js';
7
+ export { discoverEndpoints, conventionEndpoints } from './src/discovery.js';
8
+ export type { OidcEndpoints, DiscoverOptions } from './src/discovery.js';
9
+ export { resilientFetch } from './src/http/resilient_fetch.js';
10
+ export type { ResiliencePolicy } from './src/http/resilient_fetch.js';
11
+ export { AuthkitClientManager } from './providers/authkit_client_provider.js';
12
+ export type { ResolvedClientConfig, ClientConfigInput, BackchannelLogoutCallback, BackchannelLogoutInput, } from './src/define_config.js';
13
+ export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocation/revocation_store.js';
14
+ export type { RevocationStore, LucidRevocationStoreOptions, } from './src/revocation/revocation_store.js';
15
+ export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
16
+ export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
17
+ export type { UnauthorizedAccessConstructor } from './src/host/authkit_client_guard.js';
18
+ export { getAuthkit } from './src/host/request_authenticator.js';
19
+ export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
20
+ export { lucidMirror } from './src/lucid_mirror.js';
21
+ export type { LucidMirrorOptions } from './src/lucid_mirror.js';
22
+ export { registerOidcClient } from './src/register_oidc_client.js';
23
+ export type { RegisterOidcClientOptions, PostLoginRedirects, } from './src/register_oidc_client.js';
24
+ export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
25
+ export type { ValidateLogoutTokenOptions, ValidatedLogoutToken, SessionIndex, SessionIndexEntry, } from './src/backchannel_logout.js';
26
+ export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from './src/dpop.js';
27
+ export type { DpopKeyPair, CreateDpopProofInput } from './src/dpop.js';
28
+ export { verifyJwtAccessToken, clearJwksCache, } from './src/verify_access_token.js';
29
+ export type { VerifyJwtAccessTokenOptions, JwtAccessTokenClaims, } from './src/verify_access_token.js';
30
+ export type { TokenSet } from './src/types.js';
31
+ export type { Identity } from '@adonis-agora/authkit-core';
32
+ export { configure } from './commands/configure.js';
package/build/index.js CHANGED
@@ -1,15 +1,18 @@
1
- export { defineConfig, resolvers } from "./src/define_config.js";
2
- export { Authenticator } from "./src/authenticator.js";
3
- export { identityToUser, createUserinfoResolver, } from "./src/user_resolvers.js";
4
- export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from "./src/oidc_login.js";
5
- export { discoverEndpoints, conventionEndpoints } from "./src/discovery.js";
6
- export { resilientFetch } from "./src/http/resilient_fetch.js";
7
- export { AuthkitClientManager } from "./providers/authkit_client_provider.js";
8
- export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from "./src/revocation/revocation_store.js";
9
- export { default as BackchannelRevocationMiddleware } from "./src/middleware/backchannel_revocation_middleware.js";
10
- export { lucidMirror } from "./src/lucid_mirror.js";
11
- export { registerOidcClient } from "./src/register_oidc_client.js";
12
- export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from "./src/backchannel_logout.js";
13
- export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from "./src/dpop.js";
14
- export { verifyJwtAccessToken, clearJwksCache, } from "./src/verify_access_token.js";
15
- export { configure } from "./commands/configure.js";
1
+ export { defineConfig, resolvers } from './src/define_config.js';
2
+ export { Authenticator } from './src/authenticator.js';
3
+ export { identityToUser, createUserinfoResolver, } from './src/user_resolvers.js';
4
+ export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, refreshTokens, generatePkce, } from './src/oidc_login.js';
5
+ export { discoverEndpoints, conventionEndpoints } from './src/discovery.js';
6
+ export { resilientFetch } from './src/http/resilient_fetch.js';
7
+ export { AuthkitClientManager } from './providers/authkit_client_provider.js';
8
+ export { lucidRevocationStore, DEFAULT_REVOCATION_TABLE, } from './src/revocation/revocation_store.js';
9
+ export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
10
+ export { authkitClientGuard, AuthkitClientGuard } from './src/host/authkit_client_guard.js';
11
+ export { getAuthkit } from './src/host/request_authenticator.js';
12
+ export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
13
+ export { lucidMirror } from './src/lucid_mirror.js';
14
+ export { registerOidcClient } from './src/register_oidc_client.js';
15
+ export { validateLogoutToken, InvalidLogoutTokenError, InMemorySessionIndex, BACKCHANNEL_LOGOUT_EVENT, } from './src/backchannel_logout.js';
16
+ export { generateDpopKeyPair, createDpopProof, dpopJwkThumbprint, } from './src/dpop.js';
17
+ export { verifyJwtAccessToken, clearJwksCache, } from './src/verify_access_token.js';
18
+ export { configure } from './commands/configure.js';
@@ -1,7 +1,8 @@
1
- import type { HttpContext } from "@adonisjs/core/http";
2
- import type { ApplicationService } from "@adonisjs/core/types";
3
- import { Authenticator } from "../src/authenticator.js";
4
- import type { ResolvedClientConfig } from "../src/define_config.js";
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { ApplicationService } from '@adonisjs/core/types';
3
+ import { Authenticator } from '../src/authenticator.js';
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,15 +47,69 @@ 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
- declare module "@adonisjs/core/types" {
110
+ declare module '@adonisjs/core/types' {
53
111
  interface ContainerBindings {
54
- "authkit.client": AuthkitClientManager;
112
+ 'authkit.client': AuthkitClientManager;
55
113
  }
56
114
  }
57
115
  export default class AuthkitClientProvider {
@@ -1,10 +1,22 @@
1
- import { configProvider } from "@adonisjs/core";
2
- import { RuntimeException } from "@adonisjs/core/exceptions";
3
- import { Authenticator } from "../src/authenticator.js";
4
- import { validateLogoutToken } from "../src/backchannel_logout.js";
5
- import { exchangeToken, refreshTokens } from "../src/oidc_login.js";
1
+ import { randomUUID } from 'node:crypto';
2
+ import { configProvider } from '@adonisjs/core';
3
+ import { RuntimeException } from '@adonisjs/core/exceptions';
4
+ import { Authenticator } from '../src/authenticator.js';
5
+ import { validateLogoutToken } from '../src/backchannel_logout.js';
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 {
@@ -84,10 +100,10 @@ export class AuthkitClientManager {
84
100
  */
85
101
  async handleBackchannelLogout(ctx) {
86
102
  const { request, response } = ctx;
87
- response.header("Cache-Control", "no-store");
88
- const token = request.input("logout_token");
89
- if (!token || typeof token !== "string") {
90
- return response.badRequest({ error: "invalid_request" });
103
+ response.header('Cache-Control', 'no-store');
104
+ const token = request.input('logout_token');
105
+ if (!token || typeof token !== 'string') {
106
+ return response.badRequest({ error: 'invalid_request' });
91
107
  }
92
108
  let validated;
93
109
  try {
@@ -97,7 +113,7 @@ export class AuthkitClientManager {
97
113
  });
98
114
  }
99
115
  catch {
100
- return response.badRequest({ error: "invalid_request" });
116
+ return response.badRequest({ error: 'invalid_request' });
101
117
  }
102
118
  // Atualiza o índice local (se configurado) — revoga as sessões mapeadas.
103
119
  const index = this.config.sessionIndex;
@@ -126,9 +142,11 @@ export class AuthkitClientManager {
126
142
  */
127
143
  async impersonate(ctx, requestedSubject) {
128
144
  const session = ctx.session;
129
- const current = session?.get(this.config.sessionKey);
145
+ if (!session)
146
+ throw new Error('Sem sessão ativa para impersonar a partir dela');
147
+ const current = session.get(this.config.sessionKey);
130
148
  if (!current?.accessToken) {
131
- throw new Error("Sem sessão ativa para impersonar a partir dela");
149
+ throw new Error('Sem sessão ativa para impersonar a partir dela');
132
150
  }
133
151
  const impersonated = await exchangeToken({
134
152
  issuer: this.config.issuer,
@@ -138,22 +156,113 @@ export class AuthkitClientManager {
138
156
  requestedSubject,
139
157
  resilience: this.config.resilience,
140
158
  });
141
- session.put(this.#impersonatorKey, current);
142
- 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 });
143
167
  }
144
- /** 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
+ */
145
197
  async stopImpersonating(ctx) {
146
198
  const session = ctx.session;
147
- const original = session?.get(this.#impersonatorKey);
148
- if (!original)
199
+ if (!session)
200
+ return false;
201
+ const raw = session.get(this.#impersonatorKey);
202
+ if (raw === undefined || raw === null)
149
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);
211
+ return false;
212
+ }
213
+ const { impersonationBinding: _binding, ...original } = parked.tokenSet;
150
214
  session.put(this.config.sessionKey, original);
151
215
  session.forget(this.#impersonatorKey);
152
216
  return true;
153
217
  }
154
- /** 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
+ */
155
223
  isImpersonating(ctx) {
156
- 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);
157
266
  }
158
267
  async createAuthenticator(ctx) {
159
268
  const resolver = await this.#getResolver();
@@ -174,8 +283,8 @@ export default class AuthkitClientProvider {
174
283
  this.app = app;
175
284
  }
176
285
  register() {
177
- this.app.container.singleton("authkit.client", async () => {
178
- const value = this.app.config.get("authkit_client");
286
+ this.app.container.singleton('authkit.client', async () => {
287
+ const value = this.app.config.get('authkit_client');
179
288
  const config = (await configProvider.resolve(this.app, value));
180
289
  if (!config) {
181
290
  throw new RuntimeException('Config inválido em "config/authkit_client.ts". Use defineConfig de @adonis-agora/authkit-client.');
@@ -1,4 +1,4 @@
1
- import type { AuthkitClientManager } from "../providers/authkit_client_provider.js";
1
+ import type { AuthkitClientManager } from '../providers/authkit_client_provider.js';
2
2
  /**
3
3
  * Singleton accessor for the {@link AuthkitClientManager}, following the Adonis `services/main`
4
4
  * convention (like `db`, `mail`, `drive`). Lets an app write
@@ -1,4 +1,4 @@
1
- import app from "@adonisjs/core/services/app";
1
+ import app from '@adonisjs/core/services/app';
2
2
  /**
3
3
  * Singleton accessor for the {@link AuthkitClientManager}, following the Adonis `services/main`
4
4
  * convention (like `db`, `mail`, `drive`). Lets an app write
@@ -8,6 +8,6 @@ import app from "@adonisjs/core/services/app";
8
8
  */
9
9
  let manager;
10
10
  await app.booted(async () => {
11
- manager = await app.container.make("authkit.client");
11
+ manager = await app.container.make('authkit.client');
12
12
  });
13
13
  export { manager as default };
@@ -1,6 +1,6 @@
1
- import type { Identity, SessionResolver } from "@adonis-agora/authkit-core";
2
- import { type MetricsRecorder } from "@adonis-agora/authkit-core";
3
- import type { HttpContext } from "@adonisjs/core/http";
1
+ import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
2
+ import { type MetricsRecorder } from '@adonis-agora/authkit-core';
3
+ import type { HttpContext } from '@adonisjs/core/http';
4
4
  /** Contexto opcional repassado a `resolveUser` (extensão backward-compatible). */
5
5
  export interface ResolveUserContext {
6
6
  accessToken?: string;
@@ -1,5 +1,5 @@
1
- import { AUTHKIT_METRICS, NoopRecorder, } from "@adonis-agora/authkit-core";
2
- import { populateContext } from "./observability/context_bridge.js";
1
+ import { AUTHKIT_METRICS, NoopRecorder } from '@adonis-agora/authkit-core';
2
+ import { populateContext } from './observability/context_bridge.js';
3
3
  /**
4
4
  * Autenticador do request. O parâmetro `TUser` é o tipo do usuário do app que `resolveUser`
5
5
  * devolve — o app o fixa via augmentation de `HttpContext.auth` (`auth: Authenticator<AppUser>`),
@@ -47,7 +47,7 @@ export class Authenticator {
47
47
  async authenticate() {
48
48
  const identity = await this.getIdentity();
49
49
  if (!identity)
50
- throw new Error("Not authenticated");
50
+ throw new Error('Not authenticated');
51
51
  return identity;
52
52
  }
53
53
  async check() {
@@ -84,7 +84,7 @@ export class Authenticator {
84
84
  async getUserOrFail() {
85
85
  const user = await this.getUser();
86
86
  if (user === null) {
87
- throw new Error("Not authenticated: no user for the current session");
87
+ throw new Error('Not authenticated: no user for the current session');
88
88
  }
89
89
  return user;
90
90
  }
@@ -1,8 +1,8 @@
1
- import type { Identity } from "@adonis-agora/authkit-core";
2
- import type { SessionIndex } from "./backchannel_logout.js";
3
- import type { ResiliencePolicy } from "./http/resilient_fetch.js";
4
- import { type ResolverFactory, resolvers } from "./resolvers/factory.js";
5
- import type { RevocationStore } from "./revocation/revocation_store.js";
1
+ import type { Identity } from '@adonis-agora/authkit-core';
2
+ import type { SessionIndex } from './backchannel_logout.js';
3
+ import type { ResiliencePolicy } from './http/resilient_fetch.js';
4
+ import { type ResolverFactory, resolvers } from './resolvers/factory.js';
5
+ import type { RevocationStore } from './revocation/revocation_store.js';
6
6
  export { resolvers };
7
7
  /** Callback invocado quando um logout_token válido chega via Back-Channel Logout. */
8
8
  export type BackchannelLogoutCallback = (event: {
@@ -1,5 +1,5 @@
1
- import { configProvider } from "@adonisjs/core";
2
- import { resolvers } from "./resolvers/factory.js";
1
+ import { configProvider } from '@adonisjs/core';
2
+ import { resolvers } from './resolvers/factory.js';
3
3
  export { resolvers };
4
4
  export function defineConfig(config) {
5
5
  return configProvider.create(async () => {
@@ -22,9 +22,9 @@ export function defineConfig(config) {
22
22
  redirectUri: config.redirectUri,
23
23
  resolverFactory: config.resolver,
24
24
  resolveUser: config.resolveUser,
25
- sessionKey: config.sessionKey ?? "authkit",
26
- scopes: config.scopes ?? ["openid", "profile", "email", "offline_access", "roles"],
27
- globalRolesClaim: config.globalRolesClaim ?? "roles",
25
+ sessionKey: config.sessionKey ?? 'authkit',
26
+ scopes: config.scopes ?? ['openid', 'profile', 'email', 'offline_access', 'roles'],
27
+ globalRolesClaim: config.globalRolesClaim ?? 'roles',
28
28
  onBackchannelLogout,
29
29
  sessionIndex: config.sessionIndex,
30
30
  backchannelStore: store,
@@ -16,7 +16,7 @@
16
16
  * const tokens = await exchangeCode({ issuer, tokenEndpoint: endpoints.tokenEndpoint, ... })
17
17
  * ```
18
18
  */
19
- import { type ResiliencePolicy } from "./http/resilient_fetch.js";
19
+ import { type ResiliencePolicy } from './http/resilient_fetch.js';
20
20
  export interface OidcEndpoints {
21
21
  authorizationEndpoint: string;
22
22
  tokenEndpoint: string;
@@ -16,13 +16,13 @@
16
16
  * const tokens = await exchangeCode({ issuer, tokenEndpoint: endpoints.tokenEndpoint, ... })
17
17
  * ```
18
18
  */
19
- import { resilientFetch, } from "./http/resilient_fetch.js";
19
+ import { resilientFetch } from './http/resilient_fetch.js';
20
20
  /**
21
21
  * Convenção do oidc-provider (authkit-server) — usada como fallback quando o
22
22
  * documento de discovery não está acessível.
23
23
  */
24
24
  export function conventionEndpoints(issuer) {
25
- const base = issuer.replace(/\/$/, "");
25
+ const base = issuer.replace(/\/$/, '');
26
26
  return {
27
27
  authorizationEndpoint: `${base}/auth`,
28
28
  tokenEndpoint: `${base}/token`,
@@ -43,7 +43,7 @@ const cache = new Map();
43
43
  * - `overrides` sempre vencem, campo a campo.
44
44
  */
45
45
  export async function discoverEndpoints(issuer, options = {}) {
46
- const base = issuer.replace(/\/$/, "");
46
+ const base = issuer.replace(/\/$/, '');
47
47
  const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
48
48
  const cached = cache.get(base);
49
49
  let endpoints;
@@ -66,11 +66,11 @@ export async function discoverEndpoints(issuer, options = {}) {
66
66
  async function fetchDiscovery(base, fetchImpl, resilience) {
67
67
  const fallback = conventionEndpoints(base);
68
68
  try {
69
- const res = await resilientFetch(`${base}/.well-known/openid-configuration`, { headers: { accept: "application/json" } }, resilience, fetchImpl);
69
+ const res = await resilientFetch(`${base}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } }, resilience, fetchImpl);
70
70
  if (!res.ok)
71
71
  return fallback;
72
72
  const doc = (await res.json());
73
- const str = (v) => typeof v === "string" && v ? v : undefined;
73
+ const str = (v) => (typeof v === 'string' && v ? v : undefined);
74
74
  return {
75
75
  authorizationEndpoint: str(doc.authorization_endpoint) ?? fallback.authorizationEndpoint,
76
76
  tokenEndpoint: str(doc.token_endpoint) ?? fallback.tokenEndpoint,
package/build/src/dpop.js CHANGED
@@ -1,5 +1,5 @@
1
- import { SignJWT, exportJWK, generateKeyPair, calculateJwkThumbprint, base64url, } from 'jose';
2
1
  import { createHash, randomUUID } from 'node:crypto';
2
+ import { SignJWT, base64url, calculateJwkThumbprint, exportJWK, generateKeyPair, } from 'jose';
3
3
  /**
4
4
  * Gera um par de chaves DPoP ES256 (P-256), exportável como JWK. ES256 é o alg
5
5
  * recomendado pelo RFC 9449 e o que o oidc-provider do authkit-server aceita.