@adonis-agora/authkit-server 0.73.0 → 0.74.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 (35) 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/host/access_token_verifier.d.ts +100 -0
  9. package/build/src/host/access_token_verifier.js +240 -0
  10. package/build/src/host/admin_api/api_clients_controller.d.ts +4 -1
  11. package/build/src/host/admin_api/api_clients_controller.js +25 -2
  12. package/build/src/host/admin_api/dto.d.ts +1 -0
  13. package/build/src/host/admin_api/dto.js +1 -0
  14. package/build/src/host/admin_clients_service.d.ts +9 -0
  15. package/build/src/host/admin_clients_service.js +21 -0
  16. package/build/src/host/admin_console/console_clients_controller.d.ts +3 -1
  17. package/build/src/host/admin_console/console_clients_controller.js +25 -2
  18. package/build/src/host/admin_validators.d.ts +11 -1
  19. package/build/src/host/admin_validators.js +36 -8
  20. package/build/src/host/bearer_account.d.ts +9 -0
  21. package/build/src/host/bearer_account.js +28 -0
  22. package/build/src/host/client_metadata.d.ts +51 -0
  23. package/build/src/host/client_metadata.js +98 -0
  24. package/build/src/host/console_session.d.ts +14 -3
  25. package/build/src/host/console_session.js +21 -9
  26. package/build/src/host/oidc_bearer_guard.d.ts +113 -0
  27. package/build/src/host/oidc_bearer_guard.js +223 -0
  28. package/build/src/host/oidc_rp_guard.d.ts +3 -1
  29. package/build/src/host/oidc_rp_guard.js +6 -2
  30. package/build/src/host/ui-dist/assets/{index-Dct63ai-.js → index-CIqtoNcO.js} +1 -1
  31. package/build/src/host/ui-dist/index.html +1 -1
  32. package/build/src/provider/build_provider.js +42 -17
  33. package/build/src/provider/oidc_service.d.ts +8 -0
  34. package/build/src/provider/oidc_service.js +11 -1
  35. 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'],
@@ -0,0 +1,100 @@
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
+ /**
23
+ * Estratégia de verificação de access token plugável no `oidcBearerGuard`.
24
+ * `verify` devolve `null` para QUALQUER token não aceitável (desconhecido,
25
+ * expirado, revogado, assinatura inválida, sender-constrained) — o guard não
26
+ * distingue os motivos na resposta (401 genérico).
27
+ */
28
+ export interface AccessTokenVerifier {
29
+ verify(token: string): Promise<VerifiedAccessToken | null>;
30
+ /**
31
+ * Emite um access token de verdade para `accountId` — usado pelo
32
+ * `authenticateAsClient` do guard em testes. Só existe no modo in-process.
33
+ */
34
+ issue?(accountId: string, options?: IssueAccessTokenOptions): Promise<string>;
35
+ }
36
+ /** Opções de {@link AccessTokenVerifier.issue}. */
37
+ export interface IssueAccessTokenOptions {
38
+ /** Client "dono" do token. Default: `'authkit-test-client'`. */
39
+ clientId?: string;
40
+ /** Escopos do token. Default: `['openid']`. */
41
+ scopes?: string[];
42
+ }
43
+ /** O mínimo do `OidcService` que a verificação in-process usa. */
44
+ export interface InProcessIssuer {
45
+ readonly provider: any;
46
+ readonly publicJwks: {
47
+ keys: Record<string, any>[];
48
+ };
49
+ readonly config: {
50
+ issuer: string;
51
+ };
52
+ }
53
+ /**
54
+ * Verificação IN-PROCESS: o app hospeda o issuer (`authkit.server`).
55
+ *
56
+ * - token opaco → `AccessToken.find` do oidc-provider (mesma rota do
57
+ * {@link TokenVerifyService}). Revogação (`/revoke`, logout, revogação de
58
+ * grant/sessão pelo admin) apaga o artefato, então revogado = não encontrado.
59
+ * - JWT (RFC 9068, `accessTokens.format: 'jwt'`) → assinatura contra o JWKS
60
+ * PÚBLICO do provider atual, `iss` e `typ: at+jwt`. JWT AT é auto-contido: o
61
+ * oidc-provider não o persiste, então ele vale até o `exp` mesmo após revogar o
62
+ * grant — mantenha o TTL do access token curto.
63
+ *
64
+ * `resolveIssuer` é chamado a cada verificação (e não guardado): o `OidcService`
65
+ * troca a instância do provider em `reloadKeys`/rotação de chaves.
66
+ */
67
+ export declare function inProcessAccessTokenVerifier(resolveIssuer: () => Promise<InProcessIssuer>): AccessTokenVerifier;
68
+ /** Opções do modo resource server REMOTO. */
69
+ export interface RemoteAccessTokenVerifierOptions {
70
+ /** Issuer do authkit (o mesmo `issuer` do `config/authkit.ts` do IdP). */
71
+ issuer: string;
72
+ /**
73
+ * URL do JWKS. Default: o `jwks_uri` da discovery
74
+ * (`{issuer}/.well-known/openid-configuration`).
75
+ */
76
+ jwksUri?: string;
77
+ /**
78
+ * Introspecção (RFC 7662) para tokens OPACOS, autenticada com as credenciais de
79
+ * um client CONFIDENCIAL do issuer (o resource server). Sem isto, só JWT access
80
+ * tokens (RFC 9068) são aceitos.
81
+ */
82
+ introspection?: {
83
+ clientId: string;
84
+ clientSecret: string;
85
+ /** Default: o `introspection_endpoint` da discovery. */
86
+ endpoint?: string;
87
+ };
88
+ /** `fetch` alternativo (testes, proxy). Default: o global. */
89
+ fetch?: typeof fetch;
90
+ }
91
+ /**
92
+ * Verificação de RESOURCE SERVER remoto: a API NÃO hospeda o issuer.
93
+ *
94
+ * - JWT (RFC 9068) → assinatura contra o JWKS remoto (cacheado e recarregado
95
+ * pelo `jose` quando aparece um `kid` novo), `iss` e `typ: at+jwt`.
96
+ * - opaco → introspecção (RFC 7662) com client credentials; exige
97
+ * `active: true` e `token_type` Bearer (refresh token introspectado vem sem
98
+ * `token_type` e é recusado).
99
+ */
100
+ export declare function remoteAccessTokenVerifier(options: RemoteAccessTokenVerifierOptions): AccessTokenVerifier;
@@ -0,0 +1,240 @@
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
+ /** `header.payload.signature` em base64url — o formato compacto de um JWS. */
14
+ const JWS_COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
15
+ function looksLikeJwt(token) {
16
+ return JWS_COMPACT.test(token);
17
+ }
18
+ function splitScope(scope) {
19
+ return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
20
+ }
21
+ function toAudience(aud) {
22
+ if (typeof aud === 'string')
23
+ return [aud];
24
+ if (Array.isArray(aud))
25
+ return aud.filter((a) => typeof a === 'string');
26
+ return [];
27
+ }
28
+ /**
29
+ * JWT access token (RFC 9068) → {@link VerifiedAccessToken}. Recusa token com
30
+ * `cnf` (DPoP / mTLS): ele é sender-constrained e apresentá-lo como Bearer puro,
31
+ * sem a prova de posse, é exatamente o que o `cnf` existe para impedir.
32
+ */
33
+ function fromJwtPayload(payload) {
34
+ if (payload.cnf)
35
+ return null;
36
+ if (typeof payload.sub !== 'string' || !payload.sub)
37
+ return null;
38
+ const clientId = payload.client_id;
39
+ return {
40
+ format: 'jwt',
41
+ sub: payload.sub,
42
+ clientId: typeof clientId === 'string' ? clientId : null,
43
+ scopes: splitScope(payload.scope),
44
+ audience: toAudience(payload.aud),
45
+ exp: typeof payload.exp === 'number' ? payload.exp : null,
46
+ jti: typeof payload.jti === 'string' ? payload.jti : null,
47
+ };
48
+ }
49
+ /**
50
+ * Verificação IN-PROCESS: o app hospeda o issuer (`authkit.server`).
51
+ *
52
+ * - token opaco → `AccessToken.find` do oidc-provider (mesma rota do
53
+ * {@link TokenVerifyService}). Revogação (`/revoke`, logout, revogação de
54
+ * grant/sessão pelo admin) apaga o artefato, então revogado = não encontrado.
55
+ * - JWT (RFC 9068, `accessTokens.format: 'jwt'`) → assinatura contra o JWKS
56
+ * PÚBLICO do provider atual, `iss` e `typ: at+jwt`. JWT AT é auto-contido: o
57
+ * oidc-provider não o persiste, então ele vale até o `exp` mesmo após revogar o
58
+ * grant — mantenha o TTL do access token curto.
59
+ *
60
+ * `resolveIssuer` é chamado a cada verificação (e não guardado): o `OidcService`
61
+ * troca a instância do provider em `reloadKeys`/rotação de chaves.
62
+ */
63
+ export function inProcessAccessTokenVerifier(resolveIssuer) {
64
+ // JWKS local memoizado por identidade: o `publicJwks` só muda (novo objeto)
65
+ // quando o provider é reconstruído.
66
+ let cachedJwks;
67
+ let cachedKeySet;
68
+ const keySetFor = (jwks) => {
69
+ if (jwks !== cachedJwks || !cachedKeySet) {
70
+ cachedJwks = jwks;
71
+ cachedKeySet = createLocalJWKSet(jwks);
72
+ }
73
+ return cachedKeySet;
74
+ };
75
+ return {
76
+ async verify(token) {
77
+ if (!token)
78
+ return null;
79
+ const issuer = await resolveIssuer();
80
+ if (looksLikeJwt(token)) {
81
+ try {
82
+ const { payload } = await jwtVerify(token, keySetFor(issuer.publicJwks), {
83
+ issuer: issuer.config.issuer,
84
+ typ: 'at+jwt',
85
+ });
86
+ return fromJwtPayload(payload);
87
+ }
88
+ catch (error) {
89
+ if (rejectedJwt(error))
90
+ return null;
91
+ throw error;
92
+ }
93
+ }
94
+ let at;
95
+ try {
96
+ at = await issuer.provider?.AccessToken?.find(token);
97
+ }
98
+ catch (error) {
99
+ // Só erro do próprio provider (token malformado) é recusa; erro do adapter sobe.
100
+ if (error instanceof providerErrors.OIDCProviderError)
101
+ return null;
102
+ throw error;
103
+ }
104
+ if (!at)
105
+ return null;
106
+ // O oidc-provider já recusa artefato expirado no `find`; isto é guarda extra.
107
+ if (at.isExpired === true)
108
+ return null;
109
+ if (typeof at.exp === 'number' && at.exp <= Math.floor(Date.now() / 1000))
110
+ return null;
111
+ // Sender-constrained (DPoP `jkt` / mTLS `x5t#S256`): sem a prova, não aceita.
112
+ if (at.jkt || at['x5t#S256'])
113
+ return null;
114
+ if (typeof at.accountId !== 'string' || !at.accountId)
115
+ return null;
116
+ return {
117
+ format: 'opaque',
118
+ sub: at.accountId,
119
+ clientId: typeof at.clientId === 'string' ? at.clientId : null,
120
+ scopes: splitScope(at.scope),
121
+ audience: toAudience(at.aud),
122
+ exp: typeof at.exp === 'number' ? at.exp : null,
123
+ jti: typeof at.jti === 'string' ? at.jti : null,
124
+ };
125
+ },
126
+ async issue(accountId, options = {}) {
127
+ const { provider } = await resolveIssuer();
128
+ const at = new provider.AccessToken({
129
+ accountId,
130
+ clientId: options.clientId ?? 'authkit-test-client',
131
+ scope: (options.scopes ?? ['openid']).join(' '),
132
+ gty: 'authorization_code',
133
+ });
134
+ return at.save();
135
+ },
136
+ };
137
+ }
138
+ /**
139
+ * Verificação de RESOURCE SERVER remoto: a API NÃO hospeda o issuer.
140
+ *
141
+ * - JWT (RFC 9068) → assinatura contra o JWKS remoto (cacheado e recarregado
142
+ * pelo `jose` quando aparece um `kid` novo), `iss` e `typ: at+jwt`.
143
+ * - opaco → introspecção (RFC 7662) com client credentials; exige
144
+ * `active: true` e `token_type` Bearer (refresh token introspectado vem sem
145
+ * `token_type` e é recusado).
146
+ */
147
+ export function remoteAccessTokenVerifier(options) {
148
+ const doFetch = options.fetch ?? globalThis.fetch;
149
+ const issuer = options.issuer.replace(/\/+$/, '');
150
+ let discovery;
151
+ const discover = () => {
152
+ discovery ??= (async () => {
153
+ const res = await doFetch(`${issuer}/.well-known/openid-configuration`);
154
+ if (!res.ok)
155
+ throw new Error(`discovery ${res.status}`);
156
+ return (await res.json());
157
+ })().catch((error) => {
158
+ // Falha transitória não fica memoizada: a próxima request tenta de novo.
159
+ discovery = undefined;
160
+ throw error;
161
+ });
162
+ return discovery;
163
+ };
164
+ let keySet;
165
+ const remoteKeySet = async () => {
166
+ if (keySet)
167
+ return keySet;
168
+ const jwksUri = options.jwksUri ?? (await discover()).jwks_uri;
169
+ if (!jwksUri)
170
+ throw new Error('issuer sem jwks_uri');
171
+ keySet = createRemoteJWKSet(new URL(jwksUri), options.fetch ? { [customFetch]: options.fetch } : undefined);
172
+ return keySet;
173
+ };
174
+ const introspect = async (token) => {
175
+ const creds = options.introspection;
176
+ if (!creds)
177
+ return null;
178
+ const endpoint = creds.endpoint ?? (await discover()).introspection_endpoint;
179
+ if (!endpoint)
180
+ return null;
181
+ // RFC 6749 §2.3.1: client_id/secret form-urlencoded antes do Basic.
182
+ const basic = Buffer.from(`${encodeURIComponent(creds.clientId)}:${encodeURIComponent(creds.clientSecret)}`).toString('base64');
183
+ const res = await doFetch(endpoint, {
184
+ method: 'POST',
185
+ headers: {
186
+ 'content-type': 'application/x-www-form-urlencoded',
187
+ accept: 'application/json',
188
+ authorization: `Basic ${basic}`,
189
+ },
190
+ body: new URLSearchParams({ token, token_type_hint: 'access_token' }).toString(),
191
+ });
192
+ // 5xx ou credencial do resource server recusada: problema de infra/config, não do token.
193
+ if (!res.ok)
194
+ throw new Error(`authkit: introspecção falhou (HTTP ${res.status})`);
195
+ const body = (await res.json());
196
+ if (body.active !== true)
197
+ return null;
198
+ if (typeof body.token_type !== 'string' || body.token_type.toLowerCase() !== 'bearer') {
199
+ return null;
200
+ }
201
+ if (body.cnf)
202
+ return null;
203
+ if (typeof body.sub !== 'string' || !body.sub)
204
+ return null;
205
+ if (typeof body.exp === 'number' && body.exp <= Math.floor(Date.now() / 1000))
206
+ return null;
207
+ return {
208
+ format: 'opaque',
209
+ sub: body.sub,
210
+ clientId: typeof body.client_id === 'string' ? body.client_id : null,
211
+ scopes: splitScope(body.scope),
212
+ audience: toAudience(body.aud),
213
+ exp: typeof body.exp === 'number' ? body.exp : null,
214
+ jti: typeof body.jti === 'string' ? body.jti : null,
215
+ };
216
+ };
217
+ return {
218
+ async verify(token) {
219
+ if (!token)
220
+ return null;
221
+ if (looksLikeJwt(token)) {
222
+ try {
223
+ const { payload } = await jwtVerify(token, await remoteKeySet(), {
224
+ // Mesma forma da discovery (sem barra final), aceitando o `iss` com ou sem ela.
225
+ issuer: [issuer, `${issuer}/`],
226
+ typ: 'at+jwt',
227
+ });
228
+ return fromJwtPayload(payload);
229
+ }
230
+ catch (error) {
231
+ if (rejectedJwt(error))
232
+ return null;
233
+ throw error;
234
+ }
235
+ }
236
+ // Introspecção fora do ar sobe como erro (não é "token inválido").
237
+ return introspect(token);
238
+ },
239
+ };
240
+ }
@@ -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
  }>;
@@ -1,12 +1,20 @@
1
1
  import '../augmentations.js';
2
2
  import { AdminClientsService } from '../admin_clients_service.js';
3
3
  import { clientCreateInput, clientInputValidator, clientPartialInput, } from '../admin_validators.js';
4
+ import { ClientMetadataError } from '../client_metadata.js';
4
5
  import { apiError, clientDto, createdClientDto } from './dto.js';
5
6
  /** Resolve o serviço (== OidcService) + o AdminClientsService. */
6
7
  async function clientsService(ctx) {
7
8
  const service = await ctx.containerResolver.make('authkit.server');
8
9
  return { service, svc: new AdminClientsService(service) };
9
10
  }
11
+ /**
12
+ * Metadata incoerente com o tipo de client (ex.: nativo com secret, redirect
13
+ * `myapp://` num client web) — 422, igual a um input que falha no Vine.
14
+ */
15
+ function invalidMetadata(ctx, err) {
16
+ return ctx.response.unprocessableEntity(apiError('invalid_client_metadata', err.message));
17
+ }
10
18
  /**
11
19
  * Recurso de clients OIDC da Admin REST API (R6). Reaproveita o
12
20
  * {@link AdminClientsService} (mesmo que o console B6). O secret é retornado UMA vez
@@ -31,7 +39,15 @@ export default class ApiClientsController {
31
39
  async store(ctx) {
32
40
  const { service, svc } = await clientsService(ctx);
33
41
  const input = clientCreateInput(await ctx.request.validateUsing(clientInputValidator));
34
- const created = await svc.create(input);
42
+ let created;
43
+ try {
44
+ created = await svc.create(input);
45
+ }
46
+ catch (err) {
47
+ if (err instanceof ClientMetadataError)
48
+ return invalidMetadata(ctx, err);
49
+ throw err;
50
+ }
35
51
  await service.config.audit?.record({
36
52
  type: 'client.created',
37
53
  clientId: created.clientId,
@@ -47,7 +63,14 @@ export default class ApiClientsController {
47
63
  const existing = await svc.find(id);
48
64
  if (!existing)
49
65
  return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
50
- await svc.update(id, clientPartialInput(await ctx.request.validateUsing(clientInputValidator)));
66
+ try {
67
+ await svc.update(id, clientPartialInput(await ctx.request.validateUsing(clientInputValidator)));
68
+ }
69
+ catch (err) {
70
+ if (err instanceof ClientMetadataError)
71
+ return invalidMetadata(ctx, err);
72
+ throw err;
73
+ }
51
74
  await service.config.audit?.record({
52
75
  type: 'client.updated',
53
76
  clientId: id,
@@ -19,6 +19,7 @@ export declare function clientDto(client: AdminClient): {
19
19
  redirectUris: string[];
20
20
  postLogoutRedirectUris: string[];
21
21
  tokenEndpointAuthMethod: string;
22
+ applicationType: import("../client_metadata.ts").ApplicationType;
22
23
  backchannelLogoutUri: string | null;
23
24
  backchannelLogoutSessionRequired: boolean;
24
25
  };
@@ -17,6 +17,7 @@ export function clientDto(client) {
17
17
  redirectUris: client.redirectUris,
18
18
  postLogoutRedirectUris: client.postLogoutRedirectUris,
19
19
  tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
20
+ applicationType: client.applicationType,
20
21
  backchannelLogoutUri: client.backchannelLogoutUri ?? null,
21
22
  backchannelLogoutSessionRequired: client.backchannelLogoutSessionRequired ?? false,
22
23
  };