@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
@@ -0,0 +1,100 @@
1
+ import type { symbols } from '@adonisjs/auth';
2
+ import type { AuthClientResponse, GuardConfigProvider } from '@adonisjs/auth/types';
3
+ import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
4
+ import type { HttpContext } from '@adonisjs/core/http';
5
+ import type { ConfigProvider } from '@adonisjs/core/types';
6
+ import type { Authenticator } from '../authenticator.js';
7
+ /**
8
+ * Construtor do `E_UNAUTHORIZED_ACCESS` do `@adonisjs/auth`, capturado no boot
9
+ * (ver {@link authkitClientGuard}) para que `getUserOrFail()` — que o contrato
10
+ * exige SÍNCRONO — possa lançá-lo sem um `await import()`.
11
+ */
12
+ export type UnauthorizedAccessConstructor = new (message: string, options: {
13
+ guardDriverName: string;
14
+ redirectTo?: string;
15
+ }) => Error;
16
+ /** O tipo de usuário REAL por trás de um user provider do `@adonisjs/auth`. */
17
+ type RealUser<UserProvider> = UserProvider extends SessionUserProviderContract<infer User> ? User : never;
18
+ /**
19
+ * Guard de `@adonisjs/auth` apoiado na sessão OIDC do `@adonis-agora/authkit-client`.
20
+ *
21
+ * A fonte da verdade de "quem está logado" é o `SessionResolver` do authkit
22
+ * (JWT/opaco/PAT, conforme `config/authkit_client.ts`) — o guard NÃO reimplementa
23
+ * resolução de sessão nem lê a sessão por conta própria. Ele só faz a ponte:
24
+ * pega o `sub` da `Identity` (`identity.userId`) e pede o usuário do app ao
25
+ * `provider`, exatamente como o `sessionGuard` nativo faria com o id que ele
26
+ * guarda na sessão.
27
+ *
28
+ * Consequência: `middleware.auth()`, `ctx.auth.user`, `auth.check()` e o Bouncer
29
+ * passam a funcionar sobre uma sessão do authkit.
30
+ */
31
+ export declare class AuthkitClientGuard<UserProvider extends SessionUserProviderContract<unknown>> {
32
+ #private;
33
+ /** Eventos do guard. Não emitimos nenhum (ainda) — o slot existe pro contrato. */
34
+ [symbols.GUARD_KNOWN_EVENTS]: unknown;
35
+ readonly driverName = "authkit_client";
36
+ authenticationAttempted: boolean;
37
+ isAuthenticated: boolean;
38
+ user?: RealUser<UserProvider>;
39
+ constructor(_name: string, ctx: HttpContext, userProvider: UserProvider, unauthorized: UnauthorizedAccessConstructor);
40
+ /**
41
+ * O {@link Authenticator} do authkit desta request (a `Identity` crua,
42
+ * `hasGlobalRole`, `toSharedProps`). Só existe depois de uma tentativa de
43
+ * autenticação; use `ctx.authkit` (via `authkit_context_middleware`) quando
44
+ * precisar dele em rota pública.
45
+ */
46
+ get authkit(): Authenticator | undefined;
47
+ /**
48
+ * Autentica a request: resolve a sessão do authkit e mapeia o `sub` para o
49
+ * usuário do app via `provider.findById`. Lança `E_UNAUTHORIZED_ACCESS` (o
50
+ * MESMO erro dos guards nativos, então o `middleware.auth()` e o handler de
51
+ * exceção do host tratam igual) quando não há sessão OU quando a sessão é
52
+ * válida mas não existe usuário local correspondente (fail-closed).
53
+ */
54
+ authenticate(): Promise<RealUser<UserProvider>>;
55
+ /** Igual aos guards nativos: `authenticate()` sem lançar. */
56
+ check(): Promise<boolean>;
57
+ /** O usuário autenticado, ou `E_UNAUTHORIZED_ACCESS`. */
58
+ getUserOrFail(): RealUser<UserProvider>;
59
+ /**
60
+ * Não implementado de propósito: a sessão deste guard é um TokenSet OIDC, e
61
+ * forjar um `id_token` assinado pelo IdP não é algo que a lib possa fazer a
62
+ * partir de um objeto de usuário. Use `@adonis-agora/authkit-testing`
63
+ * (`mintTestIdToken`) e plante o TokenSet na sessão, ou injete um
64
+ * `fakeAuthenticator()` em `ctx.authkit`.
65
+ */
66
+ authenticateAsClient(): Promise<AuthClientResponse>;
67
+ }
68
+ /**
69
+ * Configura o {@link AuthkitClientGuard} em `config/auth.ts`:
70
+ *
71
+ * ```ts
72
+ * import { defineConfig } from '@adonisjs/auth'
73
+ * import { sessionUserProvider } from '@adonisjs/auth/session'
74
+ * import { authkitClientGuard } from '@adonis-agora/authkit-client'
75
+ *
76
+ * export default defineConfig({
77
+ * default: 'web',
78
+ * guards: {
79
+ * web: authkitClientGuard({
80
+ * provider: sessionUserProvider({ model: () => import('#models/user') }),
81
+ * }),
82
+ * },
83
+ * })
84
+ * ```
85
+ *
86
+ * O `provider` é o MESMO contrato do `sessionGuard` nativo, então qualquer user
87
+ * provider de session serve. O `findById` recebe `identity.userId` (a claim
88
+ * `sub`) — a coluna de id do model precisa ser essa (é o que o `lucidMirror()`
89
+ * grava).
90
+ *
91
+ * Precedência com `resolveUser` de `config/authkit_client.ts`: são superfícies
92
+ * SEPARADAS e ambas continuam valendo. `ctx.auth.user` (framework) vem SEMPRE do
93
+ * `provider`; `ctx.authkit.getUser()` (authkit) continua vindo do `resolveUser`.
94
+ * O guard nunca chama `resolveUser`. Com o guard configurado, o normal é apagar
95
+ * o `resolveUser` e deixar só o `provider`.
96
+ */
97
+ export declare function authkitClientGuard<UserProvider extends SessionUserProviderContract<unknown>>(config: {
98
+ provider: UserProvider | ConfigProvider<UserProvider>;
99
+ }): GuardConfigProvider<(ctx: HttpContext) => AuthkitClientGuard<UserProvider>>;
100
+ export {};
@@ -0,0 +1,153 @@
1
+ import { RuntimeException } from '@adonisjs/core/exceptions';
2
+ import { getAuthkit } from './request_authenticator.js';
3
+ /**
4
+ * Guard de `@adonisjs/auth` apoiado na sessão OIDC do `@adonis-agora/authkit-client`.
5
+ *
6
+ * A fonte da verdade de "quem está logado" é o `SessionResolver` do authkit
7
+ * (JWT/opaco/PAT, conforme `config/authkit_client.ts`) — o guard NÃO reimplementa
8
+ * resolução de sessão nem lê a sessão por conta própria. Ele só faz a ponte:
9
+ * pega o `sub` da `Identity` (`identity.userId`) e pede o usuário do app ao
10
+ * `provider`, exatamente como o `sessionGuard` nativo faria com o id que ele
11
+ * guarda na sessão.
12
+ *
13
+ * Consequência: `middleware.auth()`, `ctx.auth.user`, `auth.check()` e o Bouncer
14
+ * passam a funcionar sobre uma sessão do authkit.
15
+ */
16
+ export class AuthkitClientGuard {
17
+ #ctx;
18
+ #userProvider;
19
+ #unauthorized;
20
+ #authkit;
21
+ driverName = 'authkit_client';
22
+ authenticationAttempted = false;
23
+ isAuthenticated = false;
24
+ user;
25
+ constructor(_name, ctx, userProvider, unauthorized) {
26
+ this.#ctx = ctx;
27
+ this.#userProvider = userProvider;
28
+ this.#unauthorized = unauthorized;
29
+ }
30
+ /**
31
+ * O {@link Authenticator} do authkit desta request (a `Identity` crua,
32
+ * `hasGlobalRole`, `toSharedProps`). Só existe depois de uma tentativa de
33
+ * autenticação; use `ctx.authkit` (via `authkit_context_middleware`) quando
34
+ * precisar dele em rota pública.
35
+ */
36
+ get authkit() {
37
+ return this.#authkit;
38
+ }
39
+ #failed() {
40
+ this.isAuthenticated = false;
41
+ this.user = undefined;
42
+ return new this.#unauthorized('Invalid or expired user session', {
43
+ guardDriverName: this.driverName,
44
+ });
45
+ }
46
+ /**
47
+ * Autentica a request: resolve a sessão do authkit e mapeia o `sub` para o
48
+ * usuário do app via `provider.findById`. Lança `E_UNAUTHORIZED_ACCESS` (o
49
+ * MESMO erro dos guards nativos, então o `middleware.auth()` e o handler de
50
+ * exceção do host tratam igual) quando não há sessão OU quando a sessão é
51
+ * válida mas não existe usuário local correspondente (fail-closed).
52
+ */
53
+ async authenticate() {
54
+ if (this.authenticationAttempted)
55
+ return this.getUserOrFail();
56
+ this.authenticationAttempted = true;
57
+ const authkit = await getAuthkit(this.#ctx);
58
+ this.#authkit = authkit;
59
+ const identity = await authkit.getIdentity();
60
+ if (!identity)
61
+ throw this.#failed();
62
+ // `identity.userId` É a claim `sub` — o único id estável por conta que a
63
+ // `Identity` tem. (Ela NÃO tem `id`.)
64
+ const providerUser = await this.#userProvider.findById(identity.userId);
65
+ if (!providerUser)
66
+ throw this.#failed();
67
+ this.user = providerUser.getOriginal();
68
+ this.isAuthenticated = true;
69
+ return this.user;
70
+ }
71
+ /** Igual aos guards nativos: `authenticate()` sem lançar. */
72
+ async check() {
73
+ try {
74
+ await this.authenticate();
75
+ return true;
76
+ }
77
+ catch (error) {
78
+ if (error instanceof this.#unauthorized)
79
+ return false;
80
+ throw error;
81
+ }
82
+ }
83
+ /** O usuário autenticado, ou `E_UNAUTHORIZED_ACCESS`. */
84
+ getUserOrFail() {
85
+ if (!this.user) {
86
+ throw new this.#unauthorized('Invalid or expired user session', {
87
+ guardDriverName: this.driverName,
88
+ });
89
+ }
90
+ return this.user;
91
+ }
92
+ /**
93
+ * Não implementado de propósito: a sessão deste guard é um TokenSet OIDC, e
94
+ * forjar um `id_token` assinado pelo IdP não é algo que a lib possa fazer a
95
+ * partir de um objeto de usuário. Use `@adonis-agora/authkit-testing`
96
+ * (`mintTestIdToken`) e plante o TokenSet na sessão, ou injete um
97
+ * `fakeAuthenticator()` em `ctx.authkit`.
98
+ */
99
+ async authenticateAsClient() {
100
+ throw new RuntimeException('authkitClientGuard não suporta "loginAs" nos plugins de teste: a sessão é um TokenSet OIDC ' +
101
+ 'assinado pelo IdP. Use mintTestIdToken() de @adonis-agora/authkit-testing e plante o ' +
102
+ 'TokenSet na sessão, ou injete fakeAuthenticator() em ctx.authkit.');
103
+ }
104
+ }
105
+ /**
106
+ * Configura o {@link AuthkitClientGuard} em `config/auth.ts`:
107
+ *
108
+ * ```ts
109
+ * import { defineConfig } from '@adonisjs/auth'
110
+ * import { sessionUserProvider } from '@adonisjs/auth/session'
111
+ * import { authkitClientGuard } from '@adonis-agora/authkit-client'
112
+ *
113
+ * export default defineConfig({
114
+ * default: 'web',
115
+ * guards: {
116
+ * web: authkitClientGuard({
117
+ * provider: sessionUserProvider({ model: () => import('#models/user') }),
118
+ * }),
119
+ * },
120
+ * })
121
+ * ```
122
+ *
123
+ * O `provider` é o MESMO contrato do `sessionGuard` nativo, então qualquer user
124
+ * provider de session serve. O `findById` recebe `identity.userId` (a claim
125
+ * `sub`) — a coluna de id do model precisa ser essa (é o que o `lucidMirror()`
126
+ * grava).
127
+ *
128
+ * Precedência com `resolveUser` de `config/authkit_client.ts`: são superfícies
129
+ * SEPARADAS e ambas continuam valendo. `ctx.auth.user` (framework) vem SEMPRE do
130
+ * `provider`; `ctx.authkit.getUser()` (authkit) continua vindo do `resolveUser`.
131
+ * O guard nunca chama `resolveUser`. Com o guard configurado, o normal é apagar
132
+ * o `resolveUser` e deixar só o `provider`.
133
+ */
134
+ export function authkitClientGuard(config) {
135
+ return {
136
+ async resolver(name, app) {
137
+ let unauthorized;
138
+ try {
139
+ const auth = (await import('@adonisjs/auth'));
140
+ unauthorized = auth.errors.E_UNAUTHORIZED_ACCESS;
141
+ }
142
+ catch (error) {
143
+ throw new RuntimeException('authkitClientGuard() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
144
+ '@adonis-agora/authkit-client, só necessário se você plugar este guard em config/auth.ts). ' +
145
+ 'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
146
+ }
147
+ const provider = 'resolver' in config.provider
148
+ ? (await config.provider.resolver(app))
149
+ : config.provider;
150
+ return (ctx) => new AuthkitClientGuard(name, ctx, provider, unauthorized);
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,31 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { Authenticator } from '../authenticator.js';
3
+ /**
4
+ * Resolve (uma vez por request) o {@link Authenticator} do authkit e o memoiza
5
+ * em `ctx.authkit`.
6
+ *
7
+ * É o ÚNICO caminho de resolução de sessão da lib: passa pelo
8
+ * `AuthkitClientManager`, então o refresh proativo do token (`maybeRefresh`), o
9
+ * `SessionResolver` configurado, as métricas e a ponte do `@agora/context`
10
+ * continuam valendo — tanto para quem usa `authkit_middleware` (legado) quanto
11
+ * para quem usa o `authkitClientGuard` de `@adonisjs/auth`.
12
+ *
13
+ * Memoiza em `ctx.authkit` de propósito: quando o host registra o
14
+ * `authkit_context_middleware` E o guard, os dois compartilham a MESMA
15
+ * instância, e a sessão é resolvida (e o token renovado) uma vez só.
16
+ */
17
+ export declare function getAuthkit(ctx: HttpContext): Promise<Authenticator>;
18
+ declare module '@adonisjs/core/http' {
19
+ interface HttpContext {
20
+ /**
21
+ * Superfície específica do authkit (`getIdentity`, `hasGlobalRole`, a
22
+ * `Identity` crua). Populada pelo `authkit_context_middleware` e, no
23
+ * caminho do guard, pelo próprio `authkitClientGuard` ao autenticar.
24
+ *
25
+ * Existe para que o `@adonisjs/auth` possa ser o dono de `ctx.auth` sem que
26
+ * o host perca o que só o authkit sabe. Quem NÃO usa o guard continua com o
27
+ * `Authenticator` do authkit direto em `ctx.auth` (ver `types.ts`).
28
+ */
29
+ authkit: Authenticator;
30
+ }
31
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Resolve (uma vez por request) o {@link Authenticator} do authkit e o memoiza
3
+ * em `ctx.authkit`.
4
+ *
5
+ * É o ÚNICO caminho de resolução de sessão da lib: passa pelo
6
+ * `AuthkitClientManager`, então o refresh proativo do token (`maybeRefresh`), o
7
+ * `SessionResolver` configurado, as métricas e a ponte do `@agora/context`
8
+ * continuam valendo — tanto para quem usa `authkit_middleware` (legado) quanto
9
+ * para quem usa o `authkitClientGuard` de `@adonisjs/auth`.
10
+ *
11
+ * Memoiza em `ctx.authkit` de propósito: quando o host registra o
12
+ * `authkit_context_middleware` E o guard, os dois compartilham a MESMA
13
+ * instância, e a sessão é resolvida (e o token renovado) uma vez só.
14
+ */
15
+ export async function getAuthkit(ctx) {
16
+ const existing = ctx.authkit;
17
+ if (existing)
18
+ return existing;
19
+ const manager = await ctx.containerResolver.make('authkit.client');
20
+ await manager.maybeRefresh(ctx);
21
+ const authenticator = await manager.createAuthenticator(ctx);
22
+ ctx.authkit = authenticator;
23
+ return authenticator;
24
+ }
@@ -0,0 +1,28 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ /**
4
+ * Popula `ctx.authkit` (o `Authenticator` do authkit) e renova o token
5
+ * proativamente — SEM tocar em `ctx.auth`.
6
+ *
7
+ * É o par do `authkitClientGuard`: quem adota o guard entrega `ctx.auth` ao
8
+ * `@adonisjs/auth` (via `@adonisjs/auth/initialize_auth_middleware`) e registra
9
+ * ESTE middleware para continuar alcançando a superfície específica do authkit
10
+ * (`hasGlobalRole`, a `Identity` crua, `toSharedProps`) em qualquer rota,
11
+ * inclusive nas públicas.
12
+ *
13
+ * ```ts
14
+ * // start/kernel.ts
15
+ * router.use([
16
+ * () => import('@adonisjs/auth/initialize_auth_middleware'),
17
+ * () => import('@adonis-agora/authkit-client/authkit_context_middleware'),
18
+ * ])
19
+ * ```
20
+ *
21
+ * Não substitui o `authkit_middleware`: quem NÃO usa `@adonisjs/auth` continua
22
+ * com o `authkit_middleware`, que segue colocando o `Authenticator` em
23
+ * `ctx.auth` como sempre. Registrar os dois não faz sentido (e resolveria a
24
+ * sessão duas vezes em objetos distintos).
25
+ */
26
+ export default class AuthkitContextMiddleware {
27
+ handle(ctx: HttpContext, next: NextFn): Promise<any>;
28
+ }
@@ -0,0 +1,30 @@
1
+ import { getAuthkit } from '../host/request_authenticator.js';
2
+ /**
3
+ * Popula `ctx.authkit` (o `Authenticator` do authkit) e renova o token
4
+ * proativamente — SEM tocar em `ctx.auth`.
5
+ *
6
+ * É o par do `authkitClientGuard`: quem adota o guard entrega `ctx.auth` ao
7
+ * `@adonisjs/auth` (via `@adonisjs/auth/initialize_auth_middleware`) e registra
8
+ * ESTE middleware para continuar alcançando a superfície específica do authkit
9
+ * (`hasGlobalRole`, a `Identity` crua, `toSharedProps`) em qualquer rota,
10
+ * inclusive nas públicas.
11
+ *
12
+ * ```ts
13
+ * // start/kernel.ts
14
+ * router.use([
15
+ * () => import('@adonisjs/auth/initialize_auth_middleware'),
16
+ * () => import('@adonis-agora/authkit-client/authkit_context_middleware'),
17
+ * ])
18
+ * ```
19
+ *
20
+ * Não substitui o `authkit_middleware`: quem NÃO usa `@adonisjs/auth` continua
21
+ * com o `authkit_middleware`, que segue colocando o `Authenticator` em
22
+ * `ctx.auth` como sempre. Registrar os dois não faz sentido (e resolveria a
23
+ * sessão duas vezes em objetos distintos).
24
+ */
25
+ export default class AuthkitContextMiddleware {
26
+ async handle(ctx, next) {
27
+ await getAuthkit(ctx);
28
+ return next();
29
+ }
30
+ }
@@ -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,9 +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
- ;
30
- 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);
31
30
  }
32
31
  }
33
32
  return next();
@@ -1,4 +1,4 @@
1
- import type { Identity } from "@adonis-agora/authkit-core";
1
+ import type { Identity } from '@adonis-agora/authkit-core';
2
2
  /**
3
3
  * Popula o contexto do Agora a partir de uma identidade recém-resolvida:
4
4
  * `userRef` ({ type: 'user', id: userId }), os `globalRoles` da identidade e,
@@ -4,17 +4,17 @@
4
4
  * nunca importa `@adonis-agora/context` — e degrada para no-op quando o context
5
5
  * não está instalado. Mesmo idioma da `diagnostics_bridge`.
6
6
  */
7
- const SET_SLOT = Symbol.for("@agora/context:set");
7
+ const SET_SLOT = Symbol.for('@agora/context:set');
8
8
  /**
9
9
  * Claims OIDC candidatas a "organização/tenant ativo". A primeira presente (e
10
10
  * string não vazia) vira o `tenantId`. Não falha se nenhuma existir.
11
11
  */
12
12
  const TENANT_CLAIMS = [
13
- "active_organization_id",
14
- "org_id",
15
- "organization_id",
16
- "tenant_id",
17
- "tid",
13
+ 'active_organization_id',
14
+ 'org_id',
15
+ 'organization_id',
16
+ 'tenant_id',
17
+ 'tid',
18
18
  ];
19
19
  /** Deriva o tenant ativo das claims cruas, ou `undefined` quando ausente. */
20
20
  function deriveTenant(raw) {
@@ -22,7 +22,7 @@ function deriveTenant(raw) {
22
22
  return undefined;
23
23
  for (const claim of TENANT_CLAIMS) {
24
24
  const value = raw[claim];
25
- if (typeof value === "string" && value.length > 0)
25
+ if (typeof value === 'string' && value.length > 0)
26
26
  return value;
27
27
  }
28
28
  return undefined;
@@ -40,7 +40,7 @@ export function populateContext(identity) {
40
40
  return;
41
41
  const tenant = deriveTenant(identity.raw);
42
42
  set({
43
- userRef: { type: "user", id: identity.userId },
43
+ userRef: { type: 'user', id: identity.userId },
44
44
  globalRoles: identity.globalRoles,
45
45
  ...(tenant ? { tenantId: tenant } : {}),
46
46
  });
@@ -1,5 +1,5 @@
1
- import type { TokenSet } from "./types.js";
2
- import { type ResiliencePolicy } from "./http/resilient_fetch.js";
1
+ import { type ResiliencePolicy } from './http/resilient_fetch.js';
2
+ import type { TokenSet } from './types.js';
3
3
  /**
4
4
  * Gera um par PKCE (code_verifier + code_challenge) usando o método S256.
5
5
  *
@@ -1,6 +1,6 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { base64url } from "jose";
3
- import { resilientFetch, } from "./http/resilient_fetch.js";
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { base64url } from 'jose';
3
+ import { resilientFetch } from './http/resilient_fetch.js';
4
4
  /**
5
5
  * Gera um par PKCE (code_verifier + code_challenge) usando o método S256.
6
6
  *
@@ -11,18 +11,18 @@ import { resilientFetch, } from "./http/resilient_fetch.js";
11
11
  */
12
12
  export async function generatePkce() {
13
13
  const verifier = base64url.encode(randomBytes(32));
14
- const challenge = base64url.encode(createHash("sha256").update(verifier).digest());
15
- return { verifier, challenge, method: "S256" };
14
+ const challenge = base64url.encode(createHash('sha256').update(verifier).digest());
15
+ return { verifier, challenge, method: 'S256' };
16
16
  }
17
17
  export function buildAuthorizeUrl(p) {
18
18
  const url = new URL(p.authorizationEndpoint ?? `${p.issuer}/auth`);
19
- url.searchParams.set("client_id", p.clientId);
20
- url.searchParams.set("response_type", "code");
21
- url.searchParams.set("redirect_uri", p.redirectUri);
22
- url.searchParams.set("scope", p.scopes.join(" "));
23
- url.searchParams.set("state", p.state);
24
- url.searchParams.set("code_challenge", p.codeChallenge);
25
- url.searchParams.set("code_challenge_method", "S256");
19
+ url.searchParams.set('client_id', p.clientId);
20
+ url.searchParams.set('response_type', 'code');
21
+ url.searchParams.set('redirect_uri', p.redirectUri);
22
+ url.searchParams.set('scope', p.scopes.join(' '));
23
+ url.searchParams.set('state', p.state);
24
+ url.searchParams.set('code_challenge', p.codeChallenge);
25
+ url.searchParams.set('code_challenge_method', 'S256');
26
26
  if (p.extraParams) {
27
27
  for (const [key, value] of Object.entries(p.extraParams)) {
28
28
  if (value !== undefined && value !== null)
@@ -41,14 +41,14 @@ export function buildAuthorizeUrl(p) {
41
41
  export function buildEndSessionUrl(p) {
42
42
  const url = new URL(p.endSessionEndpoint ?? `${p.issuer}/session/end`);
43
43
  if (p.idToken)
44
- url.searchParams.set("id_token_hint", p.idToken);
44
+ url.searchParams.set('id_token_hint', p.idToken);
45
45
  if (p.postLogoutRedirectUri) {
46
- url.searchParams.set("post_logout_redirect_uri", p.postLogoutRedirectUri);
46
+ url.searchParams.set('post_logout_redirect_uri', p.postLogoutRedirectUri);
47
47
  }
48
48
  if (p.clientId)
49
- url.searchParams.set("client_id", p.clientId);
49
+ url.searchParams.set('client_id', p.clientId);
50
50
  if (p.state)
51
- url.searchParams.set("state", p.state);
51
+ url.searchParams.set('state', p.state);
52
52
  return url.toString();
53
53
  }
54
54
  /**
@@ -58,32 +58,30 @@ export function buildEndSessionUrl(p) {
58
58
  */
59
59
  async function tokenEndpoint(issuer, body, fetchImpl = fetch, endpoint, resilience) {
60
60
  const res = await resilientFetch(endpoint ?? `${issuer}/token`, {
61
- method: "POST",
62
- headers: { "content-type": "application/x-www-form-urlencoded" },
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
63
63
  body: body.toString(),
64
64
  }, resilience, fetchImpl);
65
65
  if (!res.ok)
66
66
  throw new Error(`Token endpoint returned ${res.status}`);
67
67
  const json = (await res.json());
68
68
  return {
69
- idToken: json.id_token ?? "",
69
+ idToken: json.id_token ?? '',
70
70
  accessToken: json.access_token,
71
71
  refreshToken: json.refresh_token,
72
- expiresAt: json.expires_in
73
- ? Date.now() + json.expires_in * 1000
74
- : undefined,
72
+ expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
75
73
  };
76
74
  }
77
75
  export async function exchangeCode(p) {
78
76
  const body = new URLSearchParams({
79
- grant_type: "authorization_code",
77
+ grant_type: 'authorization_code',
80
78
  code: p.code,
81
79
  redirect_uri: p.redirectUri,
82
80
  client_id: p.clientId,
83
81
  code_verifier: p.codeVerifier,
84
82
  });
85
83
  if (p.clientSecret)
86
- body.set("client_secret", p.clientSecret);
84
+ body.set('client_secret', p.clientSecret);
87
85
  return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
88
86
  }
89
87
  /**
@@ -96,28 +94,28 @@ export async function exchangeCode(p) {
96
94
  */
97
95
  export async function refreshTokens(p) {
98
96
  const body = new URLSearchParams({
99
- grant_type: "refresh_token",
97
+ grant_type: 'refresh_token',
100
98
  refresh_token: p.refreshToken,
101
99
  client_id: p.clientId,
102
100
  });
103
101
  if (p.scope)
104
- body.set("scope", p.scope);
102
+ body.set('scope', p.scope);
105
103
  if (p.clientSecret)
106
- body.set("client_secret", p.clientSecret);
104
+ body.set('client_secret', p.clientSecret);
107
105
  return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
108
106
  }
109
107
  /** Troca o token do ator por um token do usuário-alvo (RFC 8693), p/ impersonation. */
110
108
  export async function exchangeToken(p) {
111
109
  const body = new URLSearchParams({
112
- grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
110
+ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
113
111
  subject_token: p.subjectToken,
114
- subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
112
+ subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
115
113
  requested_subject: p.requestedSubject,
116
114
  client_id: p.clientId,
117
115
  });
118
116
  if (p.scope)
119
- body.set("scope", p.scope);
117
+ body.set('scope', p.scope);
120
118
  if (p.clientSecret)
121
- body.set("client_secret", p.clientSecret);
119
+ body.set('client_secret', p.clientSecret);
122
120
  return tokenEndpoint(p.issuer, body, p.fetchImpl, p.tokenEndpoint, p.resilience);
123
121
  }
@@ -17,7 +17,7 @@ export interface RegisterOidcClientOptions {
17
17
  * Hook após troca de code bem-sucedida. Retorne uma STRING para redirecionar
18
18
  * para uma rota específica; retorne void para cair em `redirects`/`/`.
19
19
  */
20
- afterLogin?: (ctx: HttpContext, identity: Identity | null) => Promise<string | void> | string | void;
20
+ afterLogin?: (ctx: HttpContext, identity: Identity | null) => Promise<string | undefined> | string | undefined;
21
21
  /** Redirect pós-login por papel (usado quando `afterLogin` não retorna string). */
22
22
  redirects?: PostLoginRedirects;
23
23
  /** Destino pós-logout. Default: origem do redirectUri + '/'. */
@@ -52,7 +52,6 @@ export function registerOidcClient(router, options = {}) {
52
52
  const pkce = ctx.session?.get(pkceKey);
53
53
  // Callback velho/expirado (state perdido ou code ausente) → recomeça o login.
54
54
  if (!pkce || pkce.state !== state || !code) {
55
- ;
56
55
  ctx.session?.forget(pkceKey);
57
56
  return ctx.response.redirect(`${prefix}/login`);
58
57
  }
@@ -64,7 +63,9 @@ export function registerOidcClient(router, options = {}) {
64
63
  code,
65
64
  codeVerifier: pkce.verifier,
66
65
  });
67
- 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);
68
69
  ctx.session?.forget(pkceKey);
69
70
  const authenticator = await manager.createAuthenticator(ctx);
70
71
  const identity = await authenticator.getIdentity();
@@ -82,10 +83,12 @@ export function registerOidcClient(router, options = {}) {
82
83
  const manager = await ctx.containerResolver.make('authkit.client');
83
84
  const cfg = manager.clientConfig;
84
85
  const idToken = manager.getIdToken(ctx);
85
- 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);
86
89
  const postLogoutRedirectUri = typeof options.postLogoutRedirect === 'function'
87
90
  ? options.postLogoutRedirect(ctx)
88
- : (options.postLogoutRedirect ?? new URL(cfg.redirectUri).origin + '/');
91
+ : (options.postLogoutRedirect ?? `${new URL(cfg.redirectUri).origin}/`);
89
92
  const endSessionUrl = buildEndSessionUrl({
90
93
  issuer: cfg.issuer,
91
94
  idToken,
@@ -1,7 +1,7 @@
1
+ import { getTokenFromSource } from '../token_source.js';
1
2
  import { JwtResolver } from './jwt_resolver.js';
2
- import { PatResolver } from './pat_resolver.js';
3
3
  import { OpaqueResolver } from './opaque_resolver.js';
4
- import { getTokenFromSource } from '../token_source.js';
4
+ import { PatResolver } from './pat_resolver.js';
5
5
  export const resolvers = {
6
6
  jwt(config = {}) {
7
7
  const tokenSource = config.tokenSource ?? 'session';
@@ -1,5 +1,5 @@
1
- import type { HttpContext } from '@adonisjs/core/http';
2
1
  import type { Identity, SessionResolver } from '@adonis-agora/authkit-core';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
3
  export interface JwtResolverConfig {
4
4
  issuer: string;
5
5
  jwksUri: string;