@adonis-agora/authkit-server 0.73.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +4 -0
  2. package/build/commands/clients_create.d.ts +2 -0
  3. package/build/commands/clients_create.js +41 -13
  4. package/build/commands/commands.json +7 -0
  5. package/build/index.d.ts +6 -0
  6. package/build/index.js +7 -0
  7. package/build/src/commands/import_clients.js +2 -0
  8. package/build/src/define_config.d.ts +8 -0
  9. package/build/src/define_config.js +1 -0
  10. package/build/src/host/access_token_verifier.d.ts +107 -0
  11. package/build/src/host/access_token_verifier.js +248 -0
  12. package/build/src/host/admin_api/api_clients_controller.d.ts +4 -1
  13. package/build/src/host/admin_api/api_clients_controller.js +25 -2
  14. package/build/src/host/admin_api/dto.d.ts +1 -0
  15. package/build/src/host/admin_api/dto.js +1 -0
  16. package/build/src/host/admin_clients_service.d.ts +9 -0
  17. package/build/src/host/admin_clients_service.js +21 -0
  18. package/build/src/host/admin_console/console_clients_controller.d.ts +3 -1
  19. package/build/src/host/admin_console/console_clients_controller.js +25 -2
  20. package/build/src/host/admin_validators.d.ts +11 -1
  21. package/build/src/host/admin_validators.js +36 -8
  22. package/build/src/host/bearer_account.d.ts +26 -0
  23. package/build/src/host/bearer_account.js +38 -0
  24. package/build/src/host/client_metadata.d.ts +51 -0
  25. package/build/src/host/client_metadata.js +98 -0
  26. package/build/src/host/console_session.d.ts +14 -3
  27. package/build/src/host/console_session.js +20 -10
  28. package/build/src/host/impersonation_session.d.ts +5 -0
  29. package/build/src/host/impersonation_session.js +37 -4
  30. package/build/src/host/oidc_bearer_guard.d.ts +113 -0
  31. package/build/src/host/oidc_bearer_guard.js +232 -0
  32. package/build/src/host/oidc_rp_guard.d.ts +3 -1
  33. package/build/src/host/oidc_rp_guard.js +6 -2
  34. package/build/src/host/ui-dist/assets/{index-Dct63ai-.js → index-CIqtoNcO.js} +1 -1
  35. package/build/src/host/ui-dist/index.html +1 -1
  36. package/build/src/provider/build_provider.js +47 -17
  37. package/build/src/provider/oidc_service.d.ts +8 -0
  38. package/build/src/provider/oidc_service.js +12 -1
  39. package/build/src/provider/token_exchange.d.ts +26 -0
  40. package/build/src/provider/token_exchange.js +44 -3
  41. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { getAccountLoginUrl } from './account_login_url.js';
2
2
  import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
3
+ import { bearerAccountId } from './bearer_account.js';
3
4
  import { impersonationState } from './impersonation_session.js';
4
5
  /**
5
6
  * Helpers públicos para integrar a sessão do console do AuthKit com
@@ -11,8 +12,18 @@ import { impersonationState } from './impersonation_session.js';
11
12
  * sessão mudar.
12
13
  */
13
14
  /**
14
- * Retorna o id da conta que o request está representando no console (ou `null`
15
- * quando não há sessão).
15
+ * Retorna o id da conta que o request está representando (ou `null` quando não
16
+ * há identidade).
17
+ *
18
+ * Fonte, nesta ordem:
19
+ * 1. a sessão do console (`ACCOUNT_SESSION_KEY`) — o comportamento de sempre;
20
+ * 2. SEM sessão logada: a conta que um `oidcBearerGuard` autenticou NESTA
21
+ * request via `Authorization: Bearer` (app nativo, SPA, serviço).
22
+ *
23
+ * O fallback (2) só existe depois que o guard bearer RODOU na request (ex.:
24
+ * `middleware.auth({ guards: ['web', 'api'] })` ou `auth.authenticateUsing`);
25
+ * um app que não usa o guard bearer continua exatamente como antes. A sessão
26
+ * sempre ganha: request com cookie de sessão E bearer responde pela sessão.
16
27
  *
17
28
  * ATENÇÃO: com impersonation ativa isto é a conta PERSONIFICADA, não o admin
18
29
  * que a personificou. É o comportamento certo para "como qual conta este
@@ -21,7 +32,7 @@ import { impersonationState } from './impersonation_session.js';
21
32
  */
22
33
  export function getAccountId(ctx) {
23
34
  const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
24
- return accountId ?? null;
35
+ return accountId ?? bearerAccountId(ctx);
25
36
  }
26
37
  /**
27
38
  * Retorna o id do HUMANO real por trás do request — o id que uma decisão de
@@ -54,18 +65,17 @@ export function getAccountId(ctx) {
54
65
  * if (!id || !(await authz.hasRole(id, 'admin'))) throw new Error('forbidden')
55
66
  */
56
67
  export function realAccountId(ctx) {
57
- // Sem sessão não há nem impersonation nem conta: mesma tolerância do
58
- // `getAccountId` (que usa `ctx.session?.`), pois `impersonationState` assume
59
- // uma sessão presente.
60
- if (!ctx.session)
61
- return null;
68
+ // `impersonationState` cobre as duas fontes: a sessão do console e o access
69
+ // token bearer trocado (`act`, ex.: app nativo) — nos dois, o humano real é o
70
+ // impersonator. Sem impersonation, a conta da sessão ou do bearer.
62
71
  return impersonationState(ctx).impersonatorId ?? getAccountId(ctx);
63
72
  }
64
73
  /**
65
- * `true` quando o request carrega uma sessão de conta do console.
74
+ * `true` quando o request carrega uma sessão de conta do console. Só a SESSÃO:
75
+ * uma identidade bearer (`oidcBearerGuard`) não conta — use `getAccountId`.
66
76
  */
67
77
  export function hasAccountSession(ctx) {
68
- return getAccountId(ctx) !== null;
78
+ return ctx.session?.get(ACCOUNT_SESSION_KEY) != null;
69
79
  }
70
80
  /**
71
81
  * URL do login do console, com `return_to` opcional de volta ao destino
@@ -61,6 +61,11 @@ export interface ImpersonationState {
61
61
  * do lado do IdP, NÃO impõe expiração na sessão (só `maxAge` faz isso).
62
62
  */
63
63
  exchangeExpiresIn?: number;
64
+ /**
65
+ * De onde veio a impersonation: `session` (console/web, keys de sessão) ou
66
+ * `bearer` (access token trocado com `act`, ex.: app nativo). Só quando `active`.
67
+ */
68
+ source?: 'session' | 'bearer';
64
69
  }
65
70
  /** Metadados aproveitados da resposta do token-exchange (só o que não é segredo). */
66
71
  export interface TokenExchangeResult {
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
3
+ import { bearerAccountId, bearerImpersonation } from './bearer_account.js';
3
4
  /**
4
5
  * Ergonômico de SESSÃO de browser no RP para "personificar" (impersonate) um
5
6
  * usuário e navegar como ele — roteado pelo token-exchange RFC 8693 que o IdP já
@@ -304,11 +305,16 @@ export async function startImpersonation(ctx, params) {
304
305
  * false }` puro quando nunca houve impersonation.
305
306
  */
306
307
  export function impersonationState(ctx) {
307
- const impersonatorId = ctx.session.get(IMPERSONATOR_SESSION_KEY);
308
- if (!impersonatorId)
309
- return { active: false };
308
+ const impersonatorId = ctx.session?.get(IMPERSONATOR_SESSION_KEY);
309
+ if (!impersonatorId) {
310
+ // Sessão de console logada manda (mesma regra do `getAccountId`): o bearer só
311
+ // conta quando a request não tem conta na sessão.
312
+ if (ctx.session?.get(ACCOUNT_SESSION_KEY))
313
+ return { active: false };
314
+ return bearerImpersonationState(ctx);
315
+ }
310
316
  const targetId = ctx.session.get(ACCOUNT_SESSION_KEY);
311
- const state = { active: true, targetId, impersonatorId };
317
+ const state = { active: true, targetId, impersonatorId, source: 'session' };
312
318
  const impersonationId = ctx.session.get(IMPERSONATION_ID_SESSION_KEY);
313
319
  if (impersonationId)
314
320
  state.impersonationId = impersonationId;
@@ -329,6 +335,33 @@ export function impersonationState(ctx) {
329
335
  }
330
336
  return state;
331
337
  }
338
+ /**
339
+ * Impersonation pelo ACCESS TOKEN (sem sessão): o `oidcBearerGuard` autenticou
340
+ * um token trocado que carrega `act`. O alvo é o `sub` do token; o impersonator,
341
+ * o `act.sub`; a expiração, o `exp` do token (o token trocado não tem refresh).
342
+ * Só existe depois que o guard bearer rodou na request, como o `getAccountId`.
343
+ */
344
+ function bearerImpersonationState(ctx) {
345
+ const bearer = bearerImpersonation(ctx);
346
+ const targetId = bearerAccountId(ctx);
347
+ if (!bearer || !targetId)
348
+ return { active: false };
349
+ const state = {
350
+ active: true,
351
+ targetId,
352
+ impersonatorId: bearer.actorId,
353
+ actSub: bearer.actorId,
354
+ source: 'bearer',
355
+ };
356
+ if (bearer.jti)
357
+ state.impersonationId = bearer.jti;
358
+ if (bearer.exp !== null) {
359
+ state.expiresAt = bearer.exp * 1000;
360
+ if (Date.now() > state.expiresAt)
361
+ state.active = false;
362
+ }
363
+ return state;
364
+ }
332
365
  /**
333
366
  * Encerra a impersonation: restaura `account_user_id = impersonator`, remove
334
367
  * TODAS as keys de sessão de impersonation (id, tempos, prova do exchange) e
@@ -0,0 +1,113 @@
1
+ import type { symbols } from '@adonisjs/auth';
2
+ import type { AuthClientResponse, GuardConfigProvider, GuardContract } 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 { EmitterLike } from '@adonisjs/core/types/events';
7
+ import { type AccessTokenVerifier, type IssueAccessTokenOptions, type RemoteAccessTokenVerifierOptions, type VerifiedAccessToken } from './access_token_verifier.js';
8
+ import { type UnauthorizedAccessConstructor } from './oidc_rp_guard.js';
9
+ type RealUser<UserProvider> = UserProvider extends SessionUserProviderContract<infer U> ? U : never;
10
+ /** Política de aceitação do token, além de "é válido e não expirou". */
11
+ export interface OidcBearerGuardPolicy {
12
+ /** Escopos exigidos (TODOS). Token sem algum deles → 401 `insufficient_scope`. */
13
+ scopes?: string[];
14
+ /**
15
+ * Audience(s) aceitas (RFC 8707 resource indicators). Quando definido, o `aud`
16
+ * do token precisa conter pelo menos uma. Token opaco emitido SEM `resource`
17
+ * não tem `aud` e é recusado.
18
+ */
19
+ audience?: string | string[];
20
+ /** Allowlist de `client_id` cujos tokens são aceitos. Ausente = qualquer client. */
21
+ clientIds?: string[];
22
+ }
23
+ export type OidcBearerGuardOptions<UserProvider extends SessionUserProviderContract<unknown>> = OidcBearerGuardPolicy & {
24
+ /**
25
+ * User provider — o MESMO tipo do `oidcRpGuard` (`sessionUserProvider(...)`
26
+ * ou `authkitUserProvider()`): o `sub` do token vai para `findById`.
27
+ */
28
+ provider: UserProvider | ConfigProvider<UserProvider>;
29
+ /**
30
+ * Modo resource server REMOTO (a API não hospeda o issuer): JWT access tokens
31
+ * via JWKS do issuer (RFC 9068) e/ou tokens opacos via introspecção (RFC 7662).
32
+ * Ausente = modo IN-PROCESS: verifica contra o issuer embarcado
33
+ * (`authkit.server`), opaco e JWT.
34
+ */
35
+ remote?: RemoteAccessTokenVerifierOptions;
36
+ /** Verificador próprio (avançado). Tem precedência sobre `remote`. */
37
+ verifier?: AccessTokenVerifier;
38
+ };
39
+ export type OidcBearerGuardEvents<User> = {
40
+ 'oidc_bearer:authentication_succeeded': {
41
+ ctx: HttpContext;
42
+ guardName: string;
43
+ user: User;
44
+ token: VerifiedAccessToken;
45
+ };
46
+ 'oidc_bearer:authentication_failed': {
47
+ ctx: HttpContext;
48
+ guardName: string;
49
+ error: Error;
50
+ };
51
+ };
52
+ /**
53
+ * Guard de `@adonisjs/auth` para APIs chamadas com access token do issuer do
54
+ * authkit — o par do {@link OidcRpGuard} para clientes SEM sessão web: apps
55
+ * nativos (React Native/Expo, RFC 8252), SPAs que falam direto com a API,
56
+ * serviços. Lê `Authorization: Bearer <token>`, verifica o token (assinatura ou
57
+ * adapter, expiração, revogação), aplica a política (escopos, audience, clients)
58
+ * e resolve o user pelo `sub` no user provider.
59
+ *
60
+ * ```ts
61
+ * // config/auth.ts
62
+ * import { oidcBearerGuard, oidcRpGuard } from '@adonis-agora/authkit-server'
63
+ * import { sessionUserProvider } from '@adonisjs/auth/session'
64
+ *
65
+ * const provider = sessionUserProvider({ model: () => import('#models/user') })
66
+ *
67
+ * const authConfig = defineConfig({
68
+ * default: 'web',
69
+ * guards: {
70
+ * web: oidcRpGuard({ provider }),
71
+ * api: oidcBearerGuard({ provider, scopes: ['openid'] }),
72
+ * },
73
+ * })
74
+ *
75
+ * // start/routes.ts — aceita sessão OU bearer:
76
+ * router.get('/me', ...).use(middleware.auth({ guards: ['web', 'api'] }))
77
+ * ```
78
+ *
79
+ * Falha SEMPRE vira `E_UNAUTHORIZED_ACCESS` (401, sem redirect), com o header
80
+ * `WWW-Authenticate: Bearer …` da RFC 6750 §3.
81
+ */
82
+ export declare class OidcBearerGuard<UserProvider extends SessionUserProviderContract<unknown>> implements GuardContract<RealUser<UserProvider>> {
83
+ #private;
84
+ [symbols.GUARD_KNOWN_EVENTS]: OidcBearerGuardEvents<RealUser<UserProvider>>;
85
+ driverName: 'oidc_bearer';
86
+ authenticationAttempted: boolean;
87
+ isAuthenticated: boolean;
88
+ user?: RealUser<UserProvider>;
89
+ /** O access token verificado desta request (após `authenticate()` com sucesso). */
90
+ accessToken?: VerifiedAccessToken;
91
+ constructor(name: string, ctx: HttpContext, emitter: EmitterLike<OidcBearerGuardEvents<RealUser<UserProvider>>>, userProvider: UserProvider, verifier: AccessTokenVerifier, policy?: OidcBearerGuardPolicy, unauthorized?: UnauthorizedAccessConstructor);
92
+ getUserOrFail(): RealUser<UserProvider>;
93
+ /** `true` quando o token autenticado tem TODOS os escopos pedidos. */
94
+ hasScopes(...scopes: string[]): boolean;
95
+ authenticate(): Promise<RealUser<UserProvider>>;
96
+ /**
97
+ * `authenticate()` sem lançar — só para falha de AUTENTICAÇÃO (mesma regra do
98
+ * `OidcRpGuard#check`): erro de infraestrutura (banco, rede) é relançado.
99
+ */
100
+ check(): Promise<boolean>;
101
+ /**
102
+ * Para testes (`client.get('/api').loginAs(user)`): emite um access token REAL
103
+ * no issuer embarcado e devolve o header `Authorization`. Só no modo in-process
104
+ * — um resource server remoto não emite tokens.
105
+ */
106
+ authenticateAsClient(user: RealUser<UserProvider>, options?: IssueAccessTokenOptions): Promise<AuthClientResponse>;
107
+ }
108
+ /**
109
+ * Factory de config pro {@link OidcBearerGuard} — mesmo padrão do `oidcRpGuard()`
110
+ * e do `sessionGuard()` nativo.
111
+ */
112
+ export declare function oidcBearerGuard<UserProvider extends SessionUserProviderContract<unknown>>(config: OidcBearerGuardOptions<UserProvider>): GuardConfigProvider<(ctx: HttpContext) => OidcBearerGuard<UserProvider>>;
113
+ export {};
@@ -0,0 +1,232 @@
1
+ import { RuntimeException } from '@adonisjs/core/exceptions';
2
+ import { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './access_token_verifier.js';
3
+ import { clearBearerAccountId, setBearerAccountId, setBearerImpersonation, } from './bearer_account.js';
4
+ import { cachedUnauthorizedAccessConstructor, loadUnauthorizedAccess, } from './oidc_rp_guard.js';
5
+ /**
6
+ * Driver do `E_UNAUTHORIZED_ACCESS` lançado pelo guard. `access_tokens` é o
7
+ * renderer do `@adonisjs/auth` para o guard NATIVO de access tokens: 401 com
8
+ * `{ errors: [{ message }] }` em JSON (ou texto/JSON:API) e NUNCA redirect — o que
9
+ * uma API consumida por app mobile espera.
10
+ */
11
+ const ERROR_RENDERER = 'access_tokens';
12
+ /** `Authorization: Bearer <token>` (RFC 6750 §2.1). Esquema case-insensitive. */
13
+ const BEARER = /^Bearer\s+([A-Za-z0-9\-._~+/]+=*)\s*$/i;
14
+ /**
15
+ * Guard de `@adonisjs/auth` para APIs chamadas com access token do issuer do
16
+ * authkit — o par do {@link OidcRpGuard} para clientes SEM sessão web: apps
17
+ * nativos (React Native/Expo, RFC 8252), SPAs que falam direto com a API,
18
+ * serviços. Lê `Authorization: Bearer <token>`, verifica o token (assinatura ou
19
+ * adapter, expiração, revogação), aplica a política (escopos, audience, clients)
20
+ * e resolve o user pelo `sub` no user provider.
21
+ *
22
+ * ```ts
23
+ * // config/auth.ts
24
+ * import { oidcBearerGuard, oidcRpGuard } from '@adonis-agora/authkit-server'
25
+ * import { sessionUserProvider } from '@adonisjs/auth/session'
26
+ *
27
+ * const provider = sessionUserProvider({ model: () => import('#models/user') })
28
+ *
29
+ * const authConfig = defineConfig({
30
+ * default: 'web',
31
+ * guards: {
32
+ * web: oidcRpGuard({ provider }),
33
+ * api: oidcBearerGuard({ provider, scopes: ['openid'] }),
34
+ * },
35
+ * })
36
+ *
37
+ * // start/routes.ts — aceita sessão OU bearer:
38
+ * router.get('/me', ...).use(middleware.auth({ guards: ['web', 'api'] }))
39
+ * ```
40
+ *
41
+ * Falha SEMPRE vira `E_UNAUTHORIZED_ACCESS` (401, sem redirect), com o header
42
+ * `WWW-Authenticate: Bearer …` da RFC 6750 §3.
43
+ */
44
+ export class OidcBearerGuard {
45
+ #name;
46
+ #ctx;
47
+ #emitter;
48
+ #userProvider;
49
+ #verifier;
50
+ #policy;
51
+ #unauthorized;
52
+ driverName = 'oidc_bearer';
53
+ authenticationAttempted = false;
54
+ isAuthenticated = false;
55
+ user;
56
+ /** O access token verificado desta request (após `authenticate()` com sucesso). */
57
+ accessToken;
58
+ constructor(name, ctx, emitter, userProvider, verifier, policy = {}, unauthorized) {
59
+ this.#name = name;
60
+ this.#ctx = ctx;
61
+ this.#emitter = emitter;
62
+ this.#userProvider = userProvider;
63
+ this.#verifier = verifier;
64
+ this.#policy = policy;
65
+ this.#unauthorized = unauthorized ?? cachedUnauthorizedAccessConstructor();
66
+ }
67
+ #unauthorizedError(message) {
68
+ const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccessConstructor();
69
+ if (!Unauthorized)
70
+ return new RuntimeException(message);
71
+ return new Unauthorized(message, { guardDriverName: ERROR_RENDERER });
72
+ }
73
+ /**
74
+ * Falha de autenticação: header `WWW-Authenticate` (RFC 6750 §3), evento, e o
75
+ * `E_UNAUTHORIZED_ACCESS`. Sem token → só o desafio `Bearer`; token recusado →
76
+ * `error="invalid_token"` / `"insufficient_scope"`.
77
+ */
78
+ #fail(bearerError) {
79
+ let challenge = 'Bearer';
80
+ if (bearerError)
81
+ challenge += ` error="${bearerError}"`;
82
+ if (bearerError === 'insufficient_scope' && this.#policy.scopes?.length) {
83
+ challenge += `, scope="${this.#policy.scopes.join(' ')}"`;
84
+ }
85
+ this.#ctx.response?.header?.('WWW-Authenticate', challenge);
86
+ clearBearerAccountId(this.#ctx);
87
+ const error = this.#unauthorizedError('Unauthorized access');
88
+ this.#emitter.emit('oidc_bearer:authentication_failed', {
89
+ ctx: this.#ctx,
90
+ guardName: this.#name,
91
+ error,
92
+ });
93
+ return error;
94
+ }
95
+ getUserOrFail() {
96
+ if (!this.user) {
97
+ throw this.#unauthorizedError('Cannot access user. Authentication has not been attempted or failed.');
98
+ }
99
+ return this.user;
100
+ }
101
+ /** `true` quando o token autenticado tem TODOS os escopos pedidos. */
102
+ hasScopes(...scopes) {
103
+ const granted = new Set(this.accessToken?.scopes ?? []);
104
+ return scopes.every((s) => granted.has(s));
105
+ }
106
+ #bearerToken() {
107
+ const header = this.#ctx.request.header('authorization');
108
+ if (!header)
109
+ return undefined;
110
+ return BEARER.exec(header)?.[1];
111
+ }
112
+ #policyError(token) {
113
+ const { clientIds, audience, scopes } = this.#policy;
114
+ if (clientIds && (!token.clientId || !clientIds.includes(token.clientId))) {
115
+ return 'invalid_token';
116
+ }
117
+ if (audience !== undefined) {
118
+ const accepted = Array.isArray(audience) ? audience : [audience];
119
+ if (!token.audience.some((a) => accepted.includes(a)))
120
+ return 'invalid_token';
121
+ }
122
+ if (scopes?.length) {
123
+ const granted = new Set(token.scopes);
124
+ if (!scopes.every((s) => granted.has(s)))
125
+ return 'insufficient_scope';
126
+ }
127
+ return null;
128
+ }
129
+ async authenticate() {
130
+ if (this.authenticationAttempted) {
131
+ return this.getUserOrFail();
132
+ }
133
+ this.authenticationAttempted = true;
134
+ this.#unauthorized ??= await loadUnauthorizedAccess('oidcBearerGuard');
135
+ const raw = this.#bearerToken();
136
+ if (!raw)
137
+ throw this.#fail();
138
+ const token = await this.#verifier.verify(raw);
139
+ if (!token)
140
+ throw this.#fail('invalid_token');
141
+ const policyError = this.#policyError(token);
142
+ if (policyError)
143
+ throw this.#fail(policyError);
144
+ const guardUser = await this.#userProvider.findById(token.sub);
145
+ if (!guardUser)
146
+ throw this.#fail('invalid_token');
147
+ // Token de impersonation: o ator (o admin) também tem que existir AGORA. Conta
148
+ // apagada/desativada depois da troca derruba o acesso na próxima request, sem
149
+ // esperar o `exp` do token.
150
+ if (token.actor && !(await this.#userProvider.findById(token.actor))) {
151
+ throw this.#fail('invalid_token');
152
+ }
153
+ this.user = guardUser.getOriginal();
154
+ this.accessToken = token;
155
+ this.isAuthenticated = true;
156
+ setBearerAccountId(this.#ctx, String(guardUser.getId()));
157
+ if (token.actor) {
158
+ setBearerImpersonation(this.#ctx, { actorId: token.actor, exp: token.exp, jti: token.jti });
159
+ }
160
+ this.#emitter.emit('oidc_bearer:authentication_succeeded', {
161
+ ctx: this.#ctx,
162
+ guardName: this.#name,
163
+ user: this.user,
164
+ token,
165
+ });
166
+ return this.user;
167
+ }
168
+ /**
169
+ * `authenticate()` sem lançar — só para falha de AUTENTICAÇÃO (mesma regra do
170
+ * `OidcRpGuard#check`): erro de infraestrutura (banco, rede) é relançado.
171
+ */
172
+ async check() {
173
+ try {
174
+ await this.authenticate();
175
+ return true;
176
+ }
177
+ catch (error) {
178
+ const Unauthorized = this.#unauthorized ?? cachedUnauthorizedAccessConstructor();
179
+ if (Unauthorized && error instanceof Unauthorized)
180
+ return false;
181
+ throw error;
182
+ }
183
+ }
184
+ /**
185
+ * Para testes (`client.get('/api').loginAs(user)`): emite um access token REAL
186
+ * no issuer embarcado e devolve o header `Authorization`. Só no modo in-process
187
+ * — um resource server remoto não emite tokens.
188
+ */
189
+ async authenticateAsClient(user, options) {
190
+ if (!this.#verifier.issue) {
191
+ throw new RuntimeException('oidcBearerGuard: authenticateAsClient só funciona no modo in-process (o app hospeda o issuer).');
192
+ }
193
+ const guardUser = await this.#userProvider.createUserForGuard(user);
194
+ const token = await this.#verifier.issue(String(guardUser.getId()), {
195
+ ...options,
196
+ scopes: options?.scopes ?? this.#policy.scopes,
197
+ });
198
+ return { headers: { authorization: `Bearer ${token}` } };
199
+ }
200
+ }
201
+ /**
202
+ * Factory de config pro {@link OidcBearerGuard} — mesmo padrão do `oidcRpGuard()`
203
+ * e do `sessionGuard()` nativo.
204
+ */
205
+ export function oidcBearerGuard(config) {
206
+ return {
207
+ async resolver(name, app) {
208
+ const emitter = await app.container.make('emitter');
209
+ const unauthorized = await loadUnauthorizedAccess('oidcBearerGuard');
210
+ let userProvider;
211
+ if (typeof config.provider.resolver === 'function') {
212
+ userProvider = await config.provider.resolver(app);
213
+ }
214
+ else {
215
+ userProvider = config.provider;
216
+ }
217
+ // In-process: o `authkit.server` é resolvido na PRIMEIRA verificação (não
218
+ // aqui) — a config do auth pode ser resolvida antes do provider do authkit
219
+ // terminar o boot.
220
+ const verifier = config.verifier ??
221
+ (config.remote
222
+ ? remoteAccessTokenVerifier(config.remote)
223
+ : inProcessAccessTokenVerifier(async () => (await app.container.make('authkit.server'))));
224
+ const policy = {
225
+ scopes: config.scopes,
226
+ audience: config.audience,
227
+ clientIds: config.clientIds,
228
+ };
229
+ return (ctx) => new OidcBearerGuard(name, ctx, emitter, userProvider, verifier, policy, unauthorized);
230
+ },
231
+ };
232
+ }
@@ -15,13 +15,15 @@ export type UnauthorizedAccessConstructor = new (message: string, options: {
15
15
  guardDriverName: string;
16
16
  redirectTo?: string;
17
17
  }) => Error;
18
+ /** O construtor já resolvido por {@link loadUnauthorizedAccess}, se houver. */
19
+ export declare function cachedUnauthorizedAccessConstructor(): UnauthorizedAccessConstructor | undefined;
18
20
  /**
19
21
  * Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
20
22
  * estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
21
23
  * mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
22
24
  * primeira `authenticate()` de um guard construído à mão.
23
25
  */
24
- export declare function loadUnauthorizedAccess(): Promise<UnauthorizedAccessConstructor>;
26
+ export declare function loadUnauthorizedAccess(guardFactoryName?: string): Promise<UnauthorizedAccessConstructor>;
25
27
  export type OidcRpGuardOptions<UserProvider extends SessionUserProviderContract<unknown>> = {
26
28
  provider: UserProvider | ConfigProvider<UserProvider>;
27
29
  sessionKey?: string;
@@ -2,13 +2,17 @@ import { RuntimeException } from '@adonisjs/core/exceptions';
2
2
  import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
3
3
  /** Memo do construtor entre instâncias — o `import()` só paga o custo uma vez. */
4
4
  let cachedUnauthorizedAccess;
5
+ /** O construtor já resolvido por {@link loadUnauthorizedAccess}, se houver. */
6
+ export function cachedUnauthorizedAccessConstructor() {
7
+ return cachedUnauthorizedAccess;
8
+ }
5
9
  /**
6
10
  * Captura o `E_UNAUTHORIZED_ACCESS` real do `@adonisjs/auth` sem import
7
11
  * estático. Chamado no boot pelo {@link oidcRpGuard} (falha cedo e com uma
8
12
  * mensagem útil se o peer não estiver instalado) e, como rede de segurança, na
9
13
  * primeira `authenticate()` de um guard construído à mão.
10
14
  */
11
- export async function loadUnauthorizedAccess() {
15
+ export async function loadUnauthorizedAccess(guardFactoryName = 'oidcRpGuard') {
12
16
  if (cachedUnauthorizedAccess)
13
17
  return cachedUnauthorizedAccess;
14
18
  try {
@@ -17,7 +21,7 @@ export async function loadUnauthorizedAccess() {
17
21
  return cachedUnauthorizedAccess;
18
22
  }
19
23
  catch (error) {
20
- throw new RuntimeException('oidcRpGuard() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
24
+ throw new RuntimeException(`${guardFactoryName}() precisa de "@adonisjs/auth" instalado (é um peer opcional do ` +
21
25
  '@adonis-agora/authkit-server, só necessário se você plugar este guard em config/auth.ts). ' +
22
26
  'Rode `npm i @adonisjs/auth` (ou pnpm/yarn).', { cause: error });
23
27
  }