@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
package/README.md CHANGED
@@ -11,6 +11,10 @@ wrapper around [`oidc-provider`](https://github.com/panva/node-oidc-provider).
11
11
  - **Passwordless** — magic-link email login and passkey-first login.
12
12
  - **Protocol extensions** — Device Flow (RFC 8628), DPoP (RFC 9449), PAR (RFC 9126), step-up
13
13
  auth via `acr_values`, Dynamic Client Registration (RFC 7591/7592).
14
+ - **Native apps** — RFC 8252 native clients (`authkit:clients:create --native`: private-use
15
+ scheme / claimed https / loopback redirects, always public + PKCE) and the `@adonisjs/auth`
16
+ guards `oidcRpGuard` (session) and `oidcBearerGuard` (`Authorization: Bearer` access tokens,
17
+ in-process or remote resource server).
14
18
  - **Admin console** — user CRUD (+ disable), client CRUD, sessions, audit log (opt-in,
15
19
  role-gated).
16
20
  - **Account console** — self-service apps/consent, security (password/email/sessions),
@@ -10,6 +10,7 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
10
10
  * node ace authkit:clients:create --client-id=my-spa --redirect-uri=https://app/cb --public
11
11
  * node ace authkit:clients:create --client-id=my-api --redirect-uri=https://api/cb --redirect-uri=https://api/cb2 --grant=client_credentials
12
12
  * node ace authkit:clients:create --redirect-uri=https://app/cb --json
13
+ * node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth
13
14
  */
14
15
  export default class AuthkitClientsCreate extends BaseCommand {
15
16
  static commandName: string;
@@ -21,6 +22,7 @@ export default class AuthkitClientsCreate extends BaseCommand {
21
22
  postLogoutUri?: string[];
22
23
  grant?: string[];
23
24
  public?: boolean;
25
+ native?: boolean;
24
26
  backchannelLogoutUri?: string;
25
27
  json?: boolean;
26
28
  run(): Promise<void>;
@@ -15,6 +15,7 @@ import { BaseCommand, flags } from '@adonisjs/core/ace';
15
15
  * node ace authkit:clients:create --client-id=my-spa --redirect-uri=https://app/cb --public
16
16
  * node ace authkit:clients:create --client-id=my-api --redirect-uri=https://api/cb --redirect-uri=https://api/cb2 --grant=client_credentials
17
17
  * node ace authkit:clients:create --redirect-uri=https://app/cb --json
18
+ * node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth
18
19
  */
19
20
  export default class AuthkitClientsCreate extends BaseCommand {
20
21
  static commandName = 'authkit:clients:create';
@@ -31,10 +32,15 @@ export default class AuthkitClientsCreate extends BaseCommand {
31
32
  ' --post-logout-uri URI(s) de post-logout redirect.',
32
33
  ' --grant Grant types. Default: authorization_code + refresh_token.',
33
34
  '',
35
+ 'App nativo (React Native/Expo, iOS, Android, desktop — RFC 8252): use --native.',
36
+ 'Implica client público (sem secret; PKCE já é obrigatório) e aceita redirect de',
37
+ 'esquema privado (com.example.app:/oauth), https claimed e loopback http://127.0.0.1.',
38
+ '',
34
39
  'Exemplos:',
35
40
  ' node ace authkit:clients:create --client-id=my-spa --redirect-uri=https://app/cb --public',
36
41
  ' node ace authkit:clients:create --redirect-uri=https://app/cb --backchannel-logout-uri=https://app/bc',
37
42
  ' node ace authkit:clients:create --client-id=my-app --redirect-uri=https://a/cb --redirect-uri=https://b/cb --json',
43
+ ' node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth',
38
44
  ];
39
45
  static options = { startApp: true };
40
46
  async run() {
@@ -48,17 +54,31 @@ export default class AuthkitClientsCreate extends BaseCommand {
48
54
  const { AdminClientsService } = await import('../src/host/admin_clients_service.js');
49
55
  const svc = new AdminClientsService(service);
50
56
  const grantTypes = this.grant && this.grant.length > 0 ? this.grant : ['authorization_code', 'refresh_token'];
51
- const tokenEndpointAuthMethod = this.public
52
- ? 'none'
53
- : 'client_secret_basic';
54
- const created = await svc.create({
55
- clientId: this.clientId,
56
- redirectUris,
57
- postLogoutRedirectUris: this.postLogoutUri ?? [],
58
- grantTypes,
59
- tokenEndpointAuthMethod,
60
- backchannelLogoutUri: this.backchannelLogoutUri,
61
- });
57
+ // App nativo é sempre público (RFC 8252 §8.5): `--native` implica `--public`.
58
+ const isPublic = !!this.public || !!this.native;
59
+ const applicationType = this.native ? 'native' : 'web';
60
+ const tokenEndpointAuthMethod = isPublic ? 'none' : 'client_secret_basic';
61
+ const { ClientMetadataError } = await import('../src/host/client_metadata.js');
62
+ let created;
63
+ try {
64
+ created = await svc.create({
65
+ clientId: this.clientId,
66
+ applicationType,
67
+ redirectUris,
68
+ postLogoutRedirectUris: this.postLogoutUri ?? [],
69
+ grantTypes,
70
+ tokenEndpointAuthMethod,
71
+ backchannelLogoutUri: this.backchannelLogoutUri,
72
+ });
73
+ }
74
+ catch (err) {
75
+ if (err instanceof ClientMetadataError) {
76
+ this.logger.logError(`❌ ${err.message}`);
77
+ this.exitCode = 1;
78
+ return;
79
+ }
80
+ throw err;
81
+ }
62
82
  if (this.json) {
63
83
  const out = {
64
84
  clientId: created.clientId,
@@ -66,7 +86,8 @@ export default class AuthkitClientsCreate extends BaseCommand {
66
86
  postLogoutRedirectUris: this.postLogoutUri ?? [],
67
87
  grantTypes,
68
88
  tokenEndpointAuthMethod,
69
- confidential: !this.public,
89
+ applicationType,
90
+ confidential: !isPublic,
70
91
  };
71
92
  if (created.clientSecret)
72
93
  out.clientSecret = created.clientSecret;
@@ -78,7 +99,9 @@ export default class AuthkitClientsCreate extends BaseCommand {
78
99
  this.logger.success(`Client criado: ${created.clientId}`);
79
100
  this.logger.info(` redirect_uris: ${redirectUris.join(', ')}`);
80
101
  this.logger.info(` grant_types: ${grantTypes.join(', ')}`);
81
- this.logger.info(` type: ${this.public ? 'publico (sem secret)' : 'confidencial'}`);
102
+ this.logger.info(` type: ${isPublic ? 'publico (sem secret)' : 'confidencial'}`);
103
+ if (this.native)
104
+ this.logger.info(' application: native (RFC 8252)');
82
105
  if (this.backchannelLogoutUri) {
83
106
  this.logger.info(` backchannel: ${this.backchannelLogoutUri}`);
84
107
  }
@@ -109,6 +132,11 @@ __decorate([
109
132
  description: 'Cria um client público (sem secret; token_endpoint_auth_method=none). Default: false (confidencial).',
110
133
  })
111
134
  ], AuthkitClientsCreate.prototype, "public", void 0);
135
+ __decorate([
136
+ flags.boolean({
137
+ description: 'Client de app nativo (application_type=native, RFC 8252): aceita redirect de esquema privado/loopback. Implica --public.',
138
+ })
139
+ ], AuthkitClientsCreate.prototype, "native", void 0);
112
140
  __decorate([
113
141
  flags.string({
114
142
  description: 'Endpoint de OIDC Back-Channel Logout do RP (POST de logout_token).',
@@ -109,6 +109,13 @@
109
109
  "type": "boolean",
110
110
  "description": "Cria um client público (sem secret; token_endpoint_auth_method=none)."
111
111
  },
112
+ {
113
+ "name": "native",
114
+ "flagName": "native",
115
+ "required": false,
116
+ "type": "boolean",
117
+ "description": "Client de app nativo (application_type=native, RFC 8252): aceita redirect de esquema privado/loopback. Implica --public."
118
+ },
112
119
  {
113
120
  "name": "backchannelLogoutUri",
114
121
  "flagName": "backchannel-logout-uri",
package/build/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export type { AdminApiConfigInput, AdminConfigInput, AuthHostRenderer, AuthMetho
16
16
  export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
17
17
  export type { EventsConfigInput, ResolvedEventsConfig } from './src/events/dispatcher.js';
18
18
  export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
19
+ export type { AccessTokenVerifier, InProcessIssuer, IssueAccessTokenOptions, RemoteAccessTokenVerifierOptions, VerifiedAccessToken, } from './src/host/access_token_verifier.js';
20
+ export { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './src/host/access_token_verifier.js';
19
21
  export type { DeletionActor, DeletionResult, } from './src/host/account_deletion_service.js';
20
22
  export { AccountDeletionService } from './src/host/account_deletion_service.js';
21
23
  export type { AccountExport } from './src/host/account_export_service.js';
@@ -59,6 +61,8 @@ export type { BotProtectionAction, BotProtectionConfigInput, BotProtectionSettin
59
61
  export { botProtectionApplies, DEFAULT_BOT_TOKEN_FIELDS, extractBotToken, guardBotProtection, resolveBotProtection, resolveEffectiveBotProtection, verifyBotProtection, } from './src/host/bot_protection.js';
60
62
  export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
61
63
  export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
64
+ export type { ApplicationType } from './src/host/client_metadata.js';
65
+ export { assertClientMetadata, ClientMetadataError, redirectUriProblem, } from './src/host/client_metadata.js';
62
66
  export type { PolicyRouteOption } from './src/host/config_locks.js';
63
67
  export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
64
68
  export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
@@ -76,6 +80,8 @@ export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonatio
76
80
  export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
77
81
  export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
78
82
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
83
+ export type { OidcBearerGuardEvents, OidcBearerGuardOptions, OidcBearerGuardPolicy, } from './src/host/oidc_bearer_guard.js';
84
+ export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.js';
79
85
  export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
80
86
  export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
81
87
  export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, type OtpLoginConfigInput, type OtpVerifyOutcome, type ResolvedOtpLoginConfig, resolveOtpLoginConfig, } from './src/host/otp_login.js';
package/build/index.js CHANGED
@@ -10,6 +10,7 @@ export { lucidStores } from './src/accounts/lucid_stores.js';
10
10
  export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
11
11
  export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
12
12
  export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
13
+ export { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './src/host/access_token_verifier.js';
13
14
  export { AccountDeletionService } from './src/host/account_deletion_service.js';
14
15
  export { AccountExportService } from './src/host/account_export_service.js';
15
16
  /**
@@ -46,6 +47,7 @@ export { computeAdminStats } from './src/host/admin_stats_service.js';
46
47
  export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
47
48
  export { botProtectionApplies, DEFAULT_BOT_TOKEN_FIELDS, extractBotToken, guardBotProtection, resolveBotProtection, resolveEffectiveBotProtection, verifyBotProtection, } from './src/host/bot_protection.js';
48
49
  export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
50
+ export { assertClientMetadata, ClientMetadataError, redirectUriProblem, } from './src/host/client_metadata.js';
49
51
  export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
50
52
  export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
51
53
  export { authkitCsrfExceptions } from './src/host/csrf.js';
@@ -60,6 +62,11 @@ export { buildImpersonationPanel } from './src/host/impersonation.js';
60
62
  export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
61
63
  // Key rotation actions — shared between the Admin REST API controller and the SDK embedded driver.
62
64
  export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
65
+ // @adonisjs/auth integration (opt-in) — guard de access token (Authorization:
66
+ // Bearer) pra APIs chamadas por apps nativos (RFC 8252), SPAs e serviços. Par do
67
+ // oidcRpGuard para clientes sem sessão web; verifica in-process (issuer embarcado)
68
+ // ou como resource server remoto (JWKS / introspecção).
69
+ export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.js';
63
70
  // @adonisjs/auth integration (opt-in) — guard pra Relying Parties OIDC.
64
71
  // O RP não autentica ninguém (sem senha); a identidade vem da sessão gravada
65
72
  // pelo callback OIDC. Plugado em config/auth.ts, faz ctx.auth.user funcionar
@@ -44,6 +44,7 @@ export async function importClients(clients, svc, options = {}) {
44
44
  }
45
45
  // Determina o auth method a partir do config estático.
46
46
  const authMethod = client.tokenEndpointAuthMethod ?? (client.clientSecret ? 'client_secret_basic' : 'none');
47
+ const applicationType = client.applicationType ?? 'web';
47
48
  try {
48
49
  // Cria via AdminClientsService, que usa o mesmo caminho do console admin / registro dinâmico.
49
50
  // Para preservar o client_secret do config estático, escrevemos diretamente via adapter —
@@ -52,6 +53,7 @@ export async function importClients(clients, svc, options = {}) {
52
53
  const preservedSecret = client.clientSecret;
53
54
  const created = await svc.createWithSecret({
54
55
  clientId,
56
+ applicationType,
55
57
  redirectUris: client.redirectUris ?? [],
56
58
  postLogoutRedirectUris: client.postLogoutRedirectUris ?? [],
57
59
  grantTypes: client.grants ?? ['authorization_code', 'refresh_token'],
@@ -622,11 +622,19 @@ export interface AdminConfigInput {
622
622
  * não altera mais o valor.
623
623
  */
624
624
  impersonation?: boolean;
625
+ /**
626
+ * Permite impersonar uma conta que também tem um dos `roles` de admin. Com
627
+ * `false`, o token-exchange recusa alvo admin (`invalid_grant`): um admin não
628
+ * assume a identidade de outro. Default: **`true`** (back-compat; virar o
629
+ * default é decisão de major, mesma regra de `impersonation`).
630
+ */
631
+ impersonateAdmins?: boolean;
625
632
  }
626
633
  export interface ResolvedAdminConfig {
627
634
  enabled: boolean;
628
635
  roles: string[];
629
636
  impersonation: boolean;
637
+ impersonateAdmins: boolean;
630
638
  }
631
639
  export declare function resolveAdmin(input?: AdminConfigInput): ResolvedAdminConfig;
632
640
  /**
@@ -174,6 +174,7 @@ export function resolveAdmin(input) {
174
174
  // Default `true`: preserva o comportamento histórico (grant sempre
175
175
  // registrado). Ver o docblock de `AdminConfigInput.impersonation`.
176
176
  impersonation: input?.impersonation !== false,
177
+ impersonateAdmins: input?.impersonateAdmins !== false,
177
178
  };
178
179
  }
179
180
  /** Lê API keys de `AUTHKIT_ADMIN_API_KEY` (uma ou várias, separadas por vírgula). */
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Access token já verificado — a forma comum que o `oidcBearerGuard` consome,
3
+ * qualquer que seja o modo de verificação (in-process ou resource server remoto)
4
+ * e o formato do token (opaco ou JWT RFC 9068).
5
+ */
6
+ export interface VerifiedAccessToken {
7
+ /** `opaque` (referência no adapter / introspecção) ou `jwt` (RFC 9068, auto-contido). */
8
+ format: 'opaque' | 'jwt';
9
+ /** `sub` — o id da conta. */
10
+ sub: string;
11
+ /** Client que recebeu o token. */
12
+ clientId: string | null;
13
+ /** Escopos concedidos. */
14
+ scopes: string[];
15
+ /** Audience(s) do token (resource indicators, RFC 8707). Vazio = sem `aud`. */
16
+ audience: string[];
17
+ /** Expiração (epoch em segundos), quando conhecida. */
18
+ exp: number | null;
19
+ /** Id do token (`jti`), quando conhecido. */
20
+ jti: string | null;
21
+ /**
22
+ * Quem está AGINDO quando o token é de impersonation (RFC 8693 `act.sub`, o
23
+ * admin que trocou o próprio token pelo do `sub`). `null` num token comum.
24
+ * É o que deixa o resource server tratar a request como impersonation
25
+ * (`impersonationState`/`realAccountId`) em vez de confundi-la com o alvo.
26
+ */
27
+ actor: string | null;
28
+ }
29
+ /**
30
+ * Estratégia de verificação de access token plugável no `oidcBearerGuard`.
31
+ * `verify` devolve `null` para QUALQUER token não aceitável (desconhecido,
32
+ * expirado, revogado, assinatura inválida, sender-constrained) — o guard não
33
+ * distingue os motivos na resposta (401 genérico).
34
+ */
35
+ export interface AccessTokenVerifier {
36
+ verify(token: string): Promise<VerifiedAccessToken | null>;
37
+ /**
38
+ * Emite um access token de verdade para `accountId` — usado pelo
39
+ * `authenticateAsClient` do guard em testes. Só existe no modo in-process.
40
+ */
41
+ issue?(accountId: string, options?: IssueAccessTokenOptions): Promise<string>;
42
+ }
43
+ /** Opções de {@link AccessTokenVerifier.issue}. */
44
+ export interface IssueAccessTokenOptions {
45
+ /** Client "dono" do token. Default: `'authkit-test-client'`. */
46
+ clientId?: string;
47
+ /** Escopos do token. Default: `['openid']`. */
48
+ scopes?: string[];
49
+ }
50
+ /** O mínimo do `OidcService` que a verificação in-process usa. */
51
+ export interface InProcessIssuer {
52
+ readonly provider: any;
53
+ readonly publicJwks: {
54
+ keys: Record<string, any>[];
55
+ };
56
+ readonly config: {
57
+ issuer: string;
58
+ };
59
+ }
60
+ /**
61
+ * Verificação IN-PROCESS: o app hospeda o issuer (`authkit.server`).
62
+ *
63
+ * - token opaco → `AccessToken.find` do oidc-provider (mesma rota do
64
+ * {@link TokenVerifyService}). Revogação (`/revoke`, logout, revogação de
65
+ * grant/sessão pelo admin) apaga o artefato, então revogado = não encontrado.
66
+ * - JWT (RFC 9068, `accessTokens.format: 'jwt'`) → assinatura contra o JWKS
67
+ * PÚBLICO do provider atual, `iss` e `typ: at+jwt`. JWT AT é auto-contido: o
68
+ * oidc-provider não o persiste, então ele vale até o `exp` mesmo após revogar o
69
+ * grant — mantenha o TTL do access token curto.
70
+ *
71
+ * `resolveIssuer` é chamado a cada verificação (e não guardado): o `OidcService`
72
+ * troca a instância do provider em `reloadKeys`/rotação de chaves.
73
+ */
74
+ export declare function inProcessAccessTokenVerifier(resolveIssuer: () => Promise<InProcessIssuer>): AccessTokenVerifier;
75
+ /** Opções do modo resource server REMOTO. */
76
+ export interface RemoteAccessTokenVerifierOptions {
77
+ /** Issuer do authkit (o mesmo `issuer` do `config/authkit.ts` do IdP). */
78
+ issuer: string;
79
+ /**
80
+ * URL do JWKS. Default: o `jwks_uri` da discovery
81
+ * (`{issuer}/.well-known/openid-configuration`).
82
+ */
83
+ jwksUri?: string;
84
+ /**
85
+ * Introspecção (RFC 7662) para tokens OPACOS, autenticada com as credenciais de
86
+ * um client CONFIDENCIAL do issuer (o resource server). Sem isto, só JWT access
87
+ * tokens (RFC 9068) são aceitos.
88
+ */
89
+ introspection?: {
90
+ clientId: string;
91
+ clientSecret: string;
92
+ /** Default: o `introspection_endpoint` da discovery. */
93
+ endpoint?: string;
94
+ };
95
+ /** `fetch` alternativo (testes, proxy). Default: o global. */
96
+ fetch?: typeof fetch;
97
+ }
98
+ /**
99
+ * Verificação de RESOURCE SERVER remoto: a API NÃO hospeda o issuer.
100
+ *
101
+ * - JWT (RFC 9068) → assinatura contra o JWKS remoto (cacheado e recarregado
102
+ * pelo `jose` quando aparece um `kid` novo), `iss` e `typ: at+jwt`.
103
+ * - opaco → introspecção (RFC 7662) com client credentials; exige
104
+ * `active: true` e `token_type` Bearer (refresh token introspectado vem sem
105
+ * `token_type` e é recusado).
106
+ */
107
+ export declare function remoteAccessTokenVerifier(options: RemoteAccessTokenVerifierOptions): AccessTokenVerifier;
@@ -0,0 +1,248 @@
1
+ import { createLocalJWKSet, createRemoteJWKSet, customFetch, errors as joseErrors, jwtVerify, } from 'jose';
2
+ import { errors as providerErrors } from 'oidc-provider';
3
+ /**
4
+ * Token recusado (assinatura, `iss`, `exp`, formato…) vira `null` → 401. Falha de
5
+ * infraestrutura (banco fora, rede, JWKS inalcançável) SOBE: responder
6
+ * `invalid_token` num apagão ensinaria o cliente a jogar fora um token bom.
7
+ */
8
+ function rejectedJwt(error) {
9
+ return (error instanceof joseErrors.JOSEError &&
10
+ !(error instanceof joseErrors.JWKSTimeout) &&
11
+ !(error instanceof joseErrors.JWKSInvalid));
12
+ }
13
+ /** `act.sub` (RFC 8693 §4.1) de um payload/`extra`/introspecção, ou `null`. */
14
+ function actorOf(source) {
15
+ const act = source?.act;
16
+ return typeof act?.sub === 'string' && act.sub ? act.sub : null;
17
+ }
18
+ /** `header.payload.signature` em base64url — o formato compacto de um JWS. */
19
+ const JWS_COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
20
+ function looksLikeJwt(token) {
21
+ return JWS_COMPACT.test(token);
22
+ }
23
+ function splitScope(scope) {
24
+ return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
25
+ }
26
+ function toAudience(aud) {
27
+ if (typeof aud === 'string')
28
+ return [aud];
29
+ if (Array.isArray(aud))
30
+ return aud.filter((a) => typeof a === 'string');
31
+ return [];
32
+ }
33
+ /**
34
+ * JWT access token (RFC 9068) → {@link VerifiedAccessToken}. Recusa token com
35
+ * `cnf` (DPoP / mTLS): ele é sender-constrained e apresentá-lo como Bearer puro,
36
+ * sem a prova de posse, é exatamente o que o `cnf` existe para impedir.
37
+ */
38
+ function fromJwtPayload(payload) {
39
+ if (payload.cnf)
40
+ return null;
41
+ if (typeof payload.sub !== 'string' || !payload.sub)
42
+ return null;
43
+ const clientId = payload.client_id;
44
+ return {
45
+ format: 'jwt',
46
+ sub: payload.sub,
47
+ clientId: typeof clientId === 'string' ? clientId : null,
48
+ scopes: splitScope(payload.scope),
49
+ audience: toAudience(payload.aud),
50
+ exp: typeof payload.exp === 'number' ? payload.exp : null,
51
+ jti: typeof payload.jti === 'string' ? payload.jti : null,
52
+ actor: actorOf(payload),
53
+ };
54
+ }
55
+ /**
56
+ * Verificação IN-PROCESS: o app hospeda o issuer (`authkit.server`).
57
+ *
58
+ * - token opaco → `AccessToken.find` do oidc-provider (mesma rota do
59
+ * {@link TokenVerifyService}). Revogação (`/revoke`, logout, revogação de
60
+ * grant/sessão pelo admin) apaga o artefato, então revogado = não encontrado.
61
+ * - JWT (RFC 9068, `accessTokens.format: 'jwt'`) → assinatura contra o JWKS
62
+ * PÚBLICO do provider atual, `iss` e `typ: at+jwt`. JWT AT é auto-contido: o
63
+ * oidc-provider não o persiste, então ele vale até o `exp` mesmo após revogar o
64
+ * grant — mantenha o TTL do access token curto.
65
+ *
66
+ * `resolveIssuer` é chamado a cada verificação (e não guardado): o `OidcService`
67
+ * troca a instância do provider em `reloadKeys`/rotação de chaves.
68
+ */
69
+ export function inProcessAccessTokenVerifier(resolveIssuer) {
70
+ // JWKS local memoizado por identidade: o `publicJwks` só muda (novo objeto)
71
+ // quando o provider é reconstruído.
72
+ let cachedJwks;
73
+ let cachedKeySet;
74
+ const keySetFor = (jwks) => {
75
+ if (jwks !== cachedJwks || !cachedKeySet) {
76
+ cachedJwks = jwks;
77
+ cachedKeySet = createLocalJWKSet(jwks);
78
+ }
79
+ return cachedKeySet;
80
+ };
81
+ return {
82
+ async verify(token) {
83
+ if (!token)
84
+ return null;
85
+ const issuer = await resolveIssuer();
86
+ if (looksLikeJwt(token)) {
87
+ try {
88
+ const { payload } = await jwtVerify(token, keySetFor(issuer.publicJwks), {
89
+ issuer: issuer.config.issuer,
90
+ typ: 'at+jwt',
91
+ });
92
+ return fromJwtPayload(payload);
93
+ }
94
+ catch (error) {
95
+ if (rejectedJwt(error))
96
+ return null;
97
+ throw error;
98
+ }
99
+ }
100
+ let at;
101
+ try {
102
+ at = await issuer.provider?.AccessToken?.find(token);
103
+ }
104
+ catch (error) {
105
+ // Só erro do próprio provider (token malformado) é recusa; erro do adapter sobe.
106
+ if (error instanceof providerErrors.OIDCProviderError)
107
+ return null;
108
+ throw error;
109
+ }
110
+ if (!at)
111
+ return null;
112
+ // O oidc-provider já recusa artefato expirado no `find`; isto é guarda extra.
113
+ if (at.isExpired === true)
114
+ return null;
115
+ if (typeof at.exp === 'number' && at.exp <= Math.floor(Date.now() / 1000))
116
+ return null;
117
+ // Sender-constrained (DPoP `jkt` / mTLS `x5t#S256`): sem a prova, não aceita.
118
+ if (at.jkt || at['x5t#S256'])
119
+ return null;
120
+ if (typeof at.accountId !== 'string' || !at.accountId)
121
+ return null;
122
+ return {
123
+ format: 'opaque',
124
+ sub: at.accountId,
125
+ clientId: typeof at.clientId === 'string' ? at.clientId : null,
126
+ scopes: splitScope(at.scope),
127
+ audience: toAudience(at.aud),
128
+ exp: typeof at.exp === 'number' ? at.exp : null,
129
+ jti: typeof at.jti === 'string' ? at.jti : null,
130
+ actor: actorOf(at.extra),
131
+ };
132
+ },
133
+ async issue(accountId, options = {}) {
134
+ const { provider } = await resolveIssuer();
135
+ const at = new provider.AccessToken({
136
+ accountId,
137
+ clientId: options.clientId ?? 'authkit-test-client',
138
+ scope: (options.scopes ?? ['openid']).join(' '),
139
+ gty: 'authorization_code',
140
+ });
141
+ return at.save();
142
+ },
143
+ };
144
+ }
145
+ /**
146
+ * Verificação de RESOURCE SERVER remoto: a API NÃO hospeda o issuer.
147
+ *
148
+ * - JWT (RFC 9068) → assinatura contra o JWKS remoto (cacheado e recarregado
149
+ * pelo `jose` quando aparece um `kid` novo), `iss` e `typ: at+jwt`.
150
+ * - opaco → introspecção (RFC 7662) com client credentials; exige
151
+ * `active: true` e `token_type` Bearer (refresh token introspectado vem sem
152
+ * `token_type` e é recusado).
153
+ */
154
+ export function remoteAccessTokenVerifier(options) {
155
+ const doFetch = options.fetch ?? globalThis.fetch;
156
+ const issuer = options.issuer.replace(/\/+$/, '');
157
+ let discovery;
158
+ const discover = () => {
159
+ discovery ??= (async () => {
160
+ const res = await doFetch(`${issuer}/.well-known/openid-configuration`);
161
+ if (!res.ok)
162
+ throw new Error(`discovery ${res.status}`);
163
+ return (await res.json());
164
+ })().catch((error) => {
165
+ // Falha transitória não fica memoizada: a próxima request tenta de novo.
166
+ discovery = undefined;
167
+ throw error;
168
+ });
169
+ return discovery;
170
+ };
171
+ let keySet;
172
+ const remoteKeySet = async () => {
173
+ if (keySet)
174
+ return keySet;
175
+ const jwksUri = options.jwksUri ?? (await discover()).jwks_uri;
176
+ if (!jwksUri)
177
+ throw new Error('issuer sem jwks_uri');
178
+ keySet = createRemoteJWKSet(new URL(jwksUri), options.fetch ? { [customFetch]: options.fetch } : undefined);
179
+ return keySet;
180
+ };
181
+ const introspect = async (token) => {
182
+ const creds = options.introspection;
183
+ if (!creds)
184
+ return null;
185
+ const endpoint = creds.endpoint ?? (await discover()).introspection_endpoint;
186
+ if (!endpoint)
187
+ return null;
188
+ // RFC 6749 §2.3.1: client_id/secret form-urlencoded antes do Basic.
189
+ const basic = Buffer.from(`${encodeURIComponent(creds.clientId)}:${encodeURIComponent(creds.clientSecret)}`).toString('base64');
190
+ const res = await doFetch(endpoint, {
191
+ method: 'POST',
192
+ headers: {
193
+ 'content-type': 'application/x-www-form-urlencoded',
194
+ accept: 'application/json',
195
+ authorization: `Basic ${basic}`,
196
+ },
197
+ body: new URLSearchParams({ token, token_type_hint: 'access_token' }).toString(),
198
+ });
199
+ // 5xx ou credencial do resource server recusada: problema de infra/config, não do token.
200
+ if (!res.ok)
201
+ throw new Error(`authkit: introspecção falhou (HTTP ${res.status})`);
202
+ const body = (await res.json());
203
+ if (body.active !== true)
204
+ return null;
205
+ if (typeof body.token_type !== 'string' || body.token_type.toLowerCase() !== 'bearer') {
206
+ return null;
207
+ }
208
+ if (body.cnf)
209
+ return null;
210
+ if (typeof body.sub !== 'string' || !body.sub)
211
+ return null;
212
+ if (typeof body.exp === 'number' && body.exp <= Math.floor(Date.now() / 1000))
213
+ return null;
214
+ return {
215
+ format: 'opaque',
216
+ sub: body.sub,
217
+ clientId: typeof body.client_id === 'string' ? body.client_id : null,
218
+ scopes: splitScope(body.scope),
219
+ audience: toAudience(body.aud),
220
+ exp: typeof body.exp === 'number' ? body.exp : null,
221
+ jti: typeof body.jti === 'string' ? body.jti : null,
222
+ actor: actorOf(body),
223
+ };
224
+ };
225
+ return {
226
+ async verify(token) {
227
+ if (!token)
228
+ return null;
229
+ if (looksLikeJwt(token)) {
230
+ try {
231
+ const { payload } = await jwtVerify(token, await remoteKeySet(), {
232
+ // Mesma forma da discovery (sem barra final), aceitando o `iss` com ou sem ela.
233
+ issuer: [issuer, `${issuer}/`],
234
+ typ: 'at+jwt',
235
+ });
236
+ return fromJwtPayload(payload);
237
+ }
238
+ catch (error) {
239
+ if (rejectedJwt(error))
240
+ return null;
241
+ throw error;
242
+ }
243
+ }
244
+ // Introspecção fora do ar sobe como erro (não é "token inválido").
245
+ return introspect(token);
246
+ },
247
+ };
248
+ }
@@ -14,6 +14,7 @@ export default class ApiClientsController {
14
14
  redirectUris: string[];
15
15
  postLogoutRedirectUris: string[];
16
16
  tokenEndpointAuthMethod: string;
17
+ applicationType: import("../client_metadata.js").ApplicationType;
17
18
  backchannelLogoutUri: string | null;
18
19
  backchannelLogoutSessionRequired: boolean;
19
20
  }[];
@@ -26,10 +27,11 @@ export default class ApiClientsController {
26
27
  redirectUris: string[];
27
28
  postLogoutRedirectUris: string[];
28
29
  tokenEndpointAuthMethod: string;
30
+ applicationType: import("../client_metadata.js").ApplicationType;
29
31
  backchannelLogoutUri: string | null;
30
32
  backchannelLogoutSessionRequired: boolean;
31
33
  }>;
32
- store(ctx: HttpContext): Promise<{
34
+ store(ctx: HttpContext): Promise<void | {
33
35
  clientId: string;
34
36
  clientSecret: string | null;
35
37
  }>;
@@ -40,6 +42,7 @@ export default class ApiClientsController {
40
42
  redirectUris: string[];
41
43
  postLogoutRedirectUris: string[];
42
44
  tokenEndpointAuthMethod: string;
45
+ applicationType: import("../client_metadata.js").ApplicationType;
43
46
  backchannelLogoutUri: string | null;
44
47
  backchannelLogoutSessionRequired: boolean;
45
48
  }>;