@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,9 +1,11 @@
1
1
  import * as oidc from 'oidc-provider';
2
2
  import { pickModelAdapterClass } from '../adapters/factory.js';
3
3
  import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
4
+ import { assertClientMetadata } from '../host/client_metadata.js';
4
5
  import { createDeviceSources } from './device_sources.js';
5
6
  import { createLogoutSources } from './logout_sources.js';
6
7
  import { registrationPolicyMiddleware } from './registration_policy.js';
8
+ import { impersonationExtraClaims } from './token_exchange.js';
7
9
  /** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
8
10
  export function updateSessionTtlHolder(holder, policy) {
9
11
  holder.rememberSec = Math.max(1, Math.floor(policy.rememberDays * 86400));
@@ -117,29 +119,49 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
117
119
  }
118
120
  : {};
119
121
  const provider = new oidc.Provider(config.issuer, {
122
+ // O ator (`act`, RFC 8693) dos access tokens de impersonation — ver
123
+ // `impersonationExtraClaims` em `token_exchange.ts`. Sem isto o token trocado
124
+ // não se distingue de um token do próprio alvo no resource server.
125
+ extraTokenClaims: async (_ctx, token) => impersonationExtraClaims(token),
120
126
  // Dispatcher por modelo (suportado pelo oidc-provider: `Adapter` aceita
121
127
  // função `(name) => adapter` além de classe). Session-scoped vai pro
122
128
  // `SessionAdapterClass`, o resto pro `AdapterClass` — mesma regra de
123
129
  // `pickModelAdapterClass`, usada também pelos serviços que instanciam
124
130
  // adapter na mão. Sem `session.adapter`, as duas classes são a mesma.
125
131
  adapter: ((name) => new (pickModelAdapterClass(name, config.AdapterClass, config.SessionAdapterClass))(name)),
126
- clients: config.clients.map((c) => ({
127
- client_id: c.clientId,
128
- client_secret: c.clientSecret,
129
- redirect_uris: c.redirectUris,
130
- post_logout_redirect_uris: c.postLogoutRedirectUris ?? [],
131
- grant_types: c.grants ?? ['authorization_code', 'refresh_token'],
132
- response_types: (c.grants ?? ['authorization_code']).includes('authorization_code')
133
- ? ['code']
134
- : [],
135
- token_endpoint_auth_method: c.tokenEndpointAuthMethod ?? (c.clientSecret ? 'client_secret_basic' : 'none'),
136
- // OIDC Back-Channel Logout: só envia as chaves quando o client as declara,
137
- // p/ não forçar metadata vazio em clients que não usam o recurso.
138
- ...(c.backchannelLogoutUri ? { backchannel_logout_uri: c.backchannelLogoutUri } : {}),
139
- ...(c.backchannelLogoutSessionRequired !== undefined
140
- ? { backchannel_logout_session_required: c.backchannelLogoutSessionRequired }
141
- : {}),
142
- })),
132
+ clients: config.clients.map((c) => {
133
+ const grants = c.grants ?? ['authorization_code', 'refresh_token'];
134
+ const tokenEndpointAuthMethod = c.tokenEndpointAuthMethod ?? (c.clientSecret ? 'client_secret_basic' : 'none');
135
+ // Clients do `defineConfig` passam pela MESMA validação do admin/CLI/import:
136
+ // um nativo com secret (ou redirect inválido pro tipo) falha no boot, não em runtime.
137
+ assertClientMetadata({
138
+ applicationType: c.applicationType ?? 'web',
139
+ tokenEndpointAuthMethod,
140
+ grantTypes: grants,
141
+ redirectUris: c.redirectUris,
142
+ postLogoutRedirectUris: c.postLogoutRedirectUris ?? [],
143
+ });
144
+ return {
145
+ client_id: c.clientId,
146
+ client_secret: c.clientSecret,
147
+ redirect_uris: c.redirectUris,
148
+ post_logout_redirect_uris: c.postLogoutRedirectUris ?? [],
149
+ grant_types: grants,
150
+ response_types: (c.grants ?? ['authorization_code']).includes('authorization_code')
151
+ ? ['code']
152
+ : [],
153
+ token_endpoint_auth_method: tokenEndpointAuthMethod,
154
+ // App nativo (RFC 8252): libera redirect de esquema privado/loopback. Sem a
155
+ // chave, o oidc-provider assume `web` — o comportamento de sempre.
156
+ ...(c.applicationType === 'native' ? { application_type: 'native' } : {}),
157
+ // OIDC Back-Channel Logout: só envia as chaves quando o client as declara,
158
+ // p/ não forçar metadata vazio em clients que não usam o recurso.
159
+ ...(c.backchannelLogoutUri ? { backchannel_logout_uri: c.backchannelLogoutUri } : {}),
160
+ ...(c.backchannelLogoutSessionRequired !== undefined
161
+ ? { backchannel_logout_session_required: c.backchannelLogoutSessionRequired }
162
+ : {}),
163
+ };
164
+ }),
143
165
  findAccount: options.findAccount,
144
166
  // Grant REAPROVEITADO (consent já lembrado): o org capturado no primeiro
145
167
  // consent ficaria VELHO se o usuário trocasse de org no console e voltasse a
@@ -220,6 +242,14 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
220
242
  // sobreviva ao authorize e fique disponivel em interactionDetails().params.audience.
221
243
  extraParams: ['audience'],
222
244
  pkce: { methods: ['S256'], required: () => true },
245
+ // Rotação SEMPRE: todo uso do refresh token emite um novo e invalida o
246
+ // anterior (reuso de um RT já rotacionado revoga o grant inteiro — detecção de
247
+ // replay do oidc-provider). Vale igual para clients públicos (SPA, app nativo
248
+ // RFC 8252), para os quais a rotação é a defesa exigida pelo OAuth 2.0 Security
249
+ // BCP, já que o RT não é amarrado a um secret. O `issueRefreshToken` fica no
250
+ // default do provider: emite RT quando o client tem o grant `refresh_token` e o
251
+ // escopo `offline_access` foi concedido (o que exige `prompt=consent` no
252
+ // authorize — OIDC Core §11), sem distinção entre client público e confidencial.
223
253
  rotateRefreshToken: true,
224
254
  features: {
225
255
  devInteractions: { enabled: false },
@@ -9,6 +9,14 @@ export declare class OidcService {
9
9
  get provider(): ReturnType<typeof buildProvider>;
10
10
  get callback(): (req: any, res: any) => void;
11
11
  get interactions(): InteractionActions;
12
+ /**
13
+ * JWKS PÚBLICO do keystore em uso pelo provider ATUAL (o mesmo que o `jwks_uri`
14
+ * serve). Troca junto com o provider num `reloadKeys`/rotação — quem verifica JWT
15
+ * in-process (ex.: `oidcBearerGuard`) lê daqui a cada verificação.
16
+ */
17
+ get publicJwks(): {
18
+ keys: Record<string, any>[];
19
+ };
12
20
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
13
21
  readonly mountPath: string;
14
22
  readonly recorder: MetricsRecorder;
@@ -4,7 +4,7 @@ import Koa from 'koa';
4
4
  import mount from 'koa-mount';
5
5
  import { normalizeActiveOrg, readActiveOrgFromKoaCtx, verifyActiveOrgMembership, } from '../host/active_org_cookie.js';
6
6
  import { isFirstPartyClient } from '../host/branding.js';
7
- import { listKeyInfos, signingKeyAgeDays } from '../keys/keystore.js';
7
+ import { listKeyInfos, signingKeyAgeDays, toPublicJwks, } from '../keys/keystore.js';
8
8
  import { wireProviderEvents } from '../observability/wire_provider_events.js';
9
9
  import { buildProvider } from './build_provider.js';
10
10
  import { createInteractionActions } from './interaction_actions.js';
@@ -13,6 +13,7 @@ export class OidcService {
13
13
  #provider;
14
14
  #callback;
15
15
  #interactions;
16
+ #publicJwks;
16
17
  #appKey;
17
18
  get provider() {
18
19
  return this.#provider;
@@ -23,6 +24,14 @@ export class OidcService {
23
24
  get interactions() {
24
25
  return this.#interactions;
25
26
  }
27
+ /**
28
+ * JWKS PÚBLICO do keystore em uso pelo provider ATUAL (o mesmo que o `jwks_uri`
29
+ * serve). Troca junto com o provider num `reloadKeys`/rotação — quem verifica JWT
30
+ * in-process (ex.: `oidcBearerGuard`) lê daqui a cada verificação.
31
+ */
32
+ get publicJwks() {
33
+ return this.#publicJwks;
34
+ }
26
35
  /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
27
36
  mountPath;
28
37
  recorder;
@@ -172,6 +181,7 @@ export class OidcService {
172
181
  // conta que acabou de desabilitar, recebendo tokens plenamente funcionais.
173
182
  accountStore: config.accountStore,
174
183
  audit: config.audit,
184
+ impersonateAdmins: config.admin.impersonateAdmins,
175
185
  });
176
186
  }
177
187
  // Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
@@ -205,6 +215,7 @@ export class OidcService {
205
215
  this.#provider = provider;
206
216
  this.#callback = callback;
207
217
  this.#interactions = interactions;
218
+ this.#publicJwks = toPublicJwks(jwks);
208
219
  }
209
220
  /**
210
221
  * Recarrega as chaves de assinatura AO VIVO: relê o keystore do cofre e reconstrói
@@ -56,5 +56,31 @@ export interface TokenExchangeDeps {
56
56
  * regra de todo o resto da lib — nunca quebra hosts com um store mínimo).
57
57
  */
58
58
  accountStore?: AccountStore;
59
+ /**
60
+ * Permite impersonar uma conta que TAMBÉM tem um dos `adminRoles`. Default
61
+ * `true` (back-compat). Com `false`, alvo admin vira `invalid_grant`: um admin
62
+ * não assume a identidade (e os tokens) de outro admin.
63
+ */
64
+ impersonateAdmins?: boolean;
59
65
  }
66
+ /**
67
+ * Propriedade (não persistida) onde a troca marca o ator ANTES do `save()`. O
68
+ * `extraTokenClaims` do provider (`build_provider.ts`) a transforma em
69
+ * `extra.act` — é o único jeito de pôr claims no `extra` de um token opaco: o
70
+ * oidc-provider SOBRESCREVE `extra` com o retorno desse hook ao salvar.
71
+ */
72
+ export declare const IMPERSONATION_ACTOR_PROP = "authkitImpersonationActor";
73
+ /** `extraTokenClaims` do provider: `{ act }` para token de impersonation, nada para o resto. */
74
+ export declare function impersonationExtraClaims(token: unknown): {
75
+ act: {
76
+ sub: string;
77
+ };
78
+ } | undefined;
79
+ /**
80
+ * O ator de um access token de impersonation: o `act` (RFC 8693 §4.1) gravado
81
+ * no `extra` do token trocado. `null` quando o token não é de impersonation.
82
+ */
83
+ export declare function impersonationActorOf(token: {
84
+ extra?: unknown;
85
+ } | null | undefined): string | null;
60
86
  export declare function registerTokenExchange(provider: any, deps: TokenExchangeDeps): void;
@@ -2,6 +2,26 @@ import { errors } from 'oidc-provider';
2
2
  import { assertAccountEnabled } from '../host/login_attempt.js';
3
3
  const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
4
4
  const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
5
+ /**
6
+ * Propriedade (não persistida) onde a troca marca o ator ANTES do `save()`. O
7
+ * `extraTokenClaims` do provider (`build_provider.ts`) a transforma em
8
+ * `extra.act` — é o único jeito de pôr claims no `extra` de um token opaco: o
9
+ * oidc-provider SOBRESCREVE `extra` com o retorno desse hook ao salvar.
10
+ */
11
+ export const IMPERSONATION_ACTOR_PROP = 'authkitImpersonationActor';
12
+ /** `extraTokenClaims` do provider: `{ act }` para token de impersonation, nada para o resto. */
13
+ export function impersonationExtraClaims(token) {
14
+ const actor = token?.[IMPERSONATION_ACTOR_PROP];
15
+ return typeof actor === 'string' && actor ? { act: { sub: actor } } : undefined;
16
+ }
17
+ /**
18
+ * O ator de um access token de impersonation: o `act` (RFC 8693 §4.1) gravado
19
+ * no `extra` do token trocado. `null` quando o token não é de impersonation.
20
+ */
21
+ export function impersonationActorOf(token) {
22
+ const act = token?.extra?.act;
23
+ return typeof act?.sub === 'string' && act.sub ? act.sub : null;
24
+ }
5
25
  /**
6
26
  * Interseção entre os scopes pedidos e os scopes permitidos do client (allowlist).
7
27
  * Preserva a ordem do pedido. Nunca excede a allowlist do client.
@@ -30,6 +50,12 @@ export function registerTokenExchange(provider, deps) {
30
50
  if (!subjectAt || subjectAt.isExpired) {
31
51
  throw new errors.InvalidGrant('subject_token invalid or expired');
32
52
  }
53
+ // Sem impersonation encadeada: um token que JÁ é de impersonation não vira
54
+ // subject de outra troca (o "ator" seria a conta personificada, e a trilha
55
+ // perderia quem de fato está agindo).
56
+ if (impersonationActorOf(subjectAt)) {
57
+ throw new errors.InvalidGrant('subject_token is already an impersonation token');
58
+ }
33
59
  // O subject_token DEVE ter sido emitido para o MESMO client autenticado: senão
34
60
  // um client B poderia trocar um AT emitido para o client A (cross-client).
35
61
  if (subjectAt.clientId !== client?.clientId) {
@@ -62,6 +88,9 @@ export function registerTokenExchange(provider, deps) {
62
88
  if (!target) {
63
89
  throw new errors.InvalidGrant('requested_subject not found');
64
90
  }
91
+ if (target.id === actor.id) {
92
+ throw new errors.InvalidGrant('requested_subject must be another account');
93
+ }
65
94
  // Status do alvo (disabled/expirado): mesmo gate de `attemptPasswordLogin`.
66
95
  // Sem isso, impersonar um alvo desabilitado mintava tokens funcionais para
67
96
  // uma identidade que o admin acreditava ter revogado. `accountStore` é
@@ -107,8 +136,6 @@ export function registerTokenExchange(provider, deps) {
107
136
  // Client sem allowlist declarada: comportamento atual preservado.
108
137
  scope = params.scope || DEFAULT_SCOPE;
109
138
  }
110
- const at = new provider.AccessToken({ accountId: target.id, client, scope });
111
- const accessToken = await at.save();
112
139
  // Token exchange is not tied to a browser session, so there is no active org
113
140
  // context here — roles are resolved for the impersonated target with clientId only.
114
141
  const roles = deps.resolveTokenRoles
@@ -117,6 +144,17 @@ export function registerTokenExchange(provider, deps) {
117
144
  activeOrg: null,
118
145
  })
119
146
  : (target.globalRoles ?? []);
147
+ if (deps.impersonateAdmins === false && roles.some((r) => adminRoles.includes(r))) {
148
+ throw new errors.InvalidGrant('requested_subject is not impersonable');
149
+ }
150
+ // O ACCESS token (não só o id_token) carrega o ator: é o que o resource server
151
+ // vê a cada request (`oidcBearerGuard` → `impersonationState`/`realAccountId`).
152
+ // Sem isso, o token trocado era indistinguível de um token do próprio alvo, e
153
+ // toda regra "negado durante impersonation" deixava passar o app nativo.
154
+ // `extra` sai no JWT (formato jwt) e na introspecção (RFC 7662).
155
+ const at = new provider.AccessToken({ accountId: target.id, client, scope });
156
+ at[IMPERSONATION_ACTOR_PROP] = actor.id;
157
+ const accessToken = await at.save();
120
158
  const idToken = new provider.IdToken({
121
159
  sub: target.id,
122
160
  email: target.email,
@@ -134,7 +172,7 @@ export function registerTokenExchange(provider, deps) {
134
172
  email: target.email ?? null,
135
173
  clientId: client?.clientId ?? null,
136
174
  ip: ctx.req?.socket?.remoteAddress ?? null,
137
- metadata: { scope },
175
+ metadata: { scope, jti: at.jti ?? null },
138
176
  });
139
177
  ctx.body = {
140
178
  access_token: accessToken,
@@ -143,6 +181,9 @@ export function registerTokenExchange(provider, deps) {
143
181
  expires_in: at.expiration ?? 3600,
144
182
  id_token: idTokenJwt,
145
183
  scope,
184
+ // RFC 8693 §4.1: o ator também no corpo da resposta (o `actSub` que o
185
+ // `requestTokenExchange` do host lê — antes ficava sempre vazio).
186
+ act: { sub: actor.id },
146
187
  };
147
188
  };
148
189
  provider.registerGrantType(TOKEN_EXCHANGE, handler, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -102,7 +102,7 @@
102
102
  }
103
103
  },
104
104
  "dependencies": {
105
- "@adonis-agora/authkit-core": "0.8.0",
105
+ "@adonis-agora/authkit-core": "0.9.0",
106
106
  "@simplewebauthn/browser": "14.0.0",
107
107
  "@simplewebauthn/server": "14.0.2",
108
108
  "jose": "6.2.12",
@@ -113,7 +113,7 @@
113
113
  "qrcode": "1.5.4"
114
114
  },
115
115
  "devDependencies": {
116
- "@adonis-agora/authkit-react": "0.22.0",
116
+ "@adonis-agora/authkit-react": "0.23.0",
117
117
  "@adonis-agora/durable": "0.40.0",
118
118
  "@adonis-agora/telescope": "0.21.1",
119
119
  "@adonisjs/ally": "6.3.0",