Agora

Dynamic Client Registration

RFC 7591/7592 — register OIDC clients at runtime.

By default AuthKit's clients are the static list in defineConfig. With Dynamic Client Registration (RFC 7591) enabled, the provider also exposes a registration endpoint, and clients created there are persisted by the same adapter used for the other OIDC artifacts — so dynamic clients coexist with the static ones.

Enabling it

config/authkit.ts
defineConfig({
  // ...
  dynamicRegistration: {
    enabled: true,
    initialAccessToken: env.get('AUTHKIT_REGISTRATION_IAT'), // strongly recommended
    management: true, // RFC 7592 read/update/delete
  },
})
FieldTypeDefaultNotes
enabledbooleanfalseExposes the registration endpoint (oidc-provider's registration feature).
initialAccessTokenstring?—Requires this Initial Access Token (IAT) as a bearer to register (RFC 7591 §3). When absent, registration is open.
managementbooleanfalseEnables Registration Management (RFC 7592): read/update/delete a registered client via its registration_access_token.
redirectUriPolicyRedirectUriPolicy | falseopen registration: { loopback: true }; with an IAT: noneWhere the authorization code may go. See Redirect URI policy.
validateRegistration(metadata, { operation, ctx }) => void | metadata—Host hook run after the redirect policy, on create and on RFC 7592 update.

Without an initialAccessToken, registration is open — anyone can register a client. That is rarely desirable in production; always set an IAT unless you have another gate in front of the endpoint.

Redirect URI policy

On its own, oidc-provider accepts any syntactically valid redirect — any https:// URL included. With open registration that lets anyone register a client whose callback is an attacker's domain and use your consent screen as bait. What protects the user is where the authorization code can go, so AuthKit applies a policy before the provider, on POST /reg and on the RFC 7592 PUT /reg/:clientId:

  1. every redirect_uris / post_logout_redirect_uris entry must match the policy;
  2. the client is restricted to the code flow: grant_types ⊆ authorization_code, refresh_token and response_types = ['code'] (PKCE is already mandatory);
  3. a client whose redirects are all loopback / private-use schemes is registered as application_type: 'native' (unless it says otherwise);
  4. validateRegistration runs, if configured.

A rejected registration gets 400 { "error": "invalid_redirect_uri" | "invalid_client_metadata" }.

config/authkit.ts
dynamicRegistration: {
  enabled: true, // open: MCP clients cannot obtain an IAT
  redirectUriPolicy: {
    loopback: true, // http://localhost|127.0.0.1|[::1], any port and path (RFC 8252 §7.3)
    exact: [
      'https://claude.ai/api/mcp/auth_callback',
      'https://chatgpt.com/connector_platform_oauth_redirect',
      'https://vscode.dev/redirect',
    ],
    appSchemes: ['cursor', 'vscode'], // private-use URI schemes (RFC 8252 §7.1)
  },
},
FieldTypeDefaultNotes
loopbackbooleantruePlain-http loopback hosts on any port. Embedded credentials and fragments are always rejected.
exactstring[][]Redirects accepted by exact string equality.
appSchemesstring[][]Private-use schemes, without :.
anyHttpsbooleanfalseAccept any https:// redirect — the unrestricted provider behaviour.

Defaults. With open registration (no initialAccessToken) the policy defaults to loopback only. With an initialAccessToken there is no policy unless you declare one — whoever holds the IAT is trusted. redirectUriPolicy: false switches the policy off explicitly.

Before this policy existed, open registration accepted any redirect. If you run open registration for web clients, list their callbacks in exact (or set anyHttps: true knowingly).

Custom validation

validateRegistration receives the (already policy-checked) metadata. Return nothing to accept it, return an object to replace it, or throw RegistrationPolicyError to reject:

import { RegistrationPolicyError } from '@adonis-agora/authkit-server'

dynamicRegistration: {
  enabled: true,
  validateRegistration: (metadata, { operation }) => {
    if (typeof metadata.client_name !== 'string') {
      throw new RegistrationPolicyError('invalid_client_metadata', 'client_name is required')
    }
  },
},

Registering a client

Post the client metadata to the provider's registration endpoint (mounted under the issuer, e.g. ${issuer}/reg), carrying the IAT as a bearer when configured:

curl -X POST "$AUTHKIT_ISSUER/reg" \
  -H "Authorization: Bearer $AUTHKIT_REGISTRATION_IAT" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Integration",
    "redirect_uris": ["https://app.example.com/auth/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "token_endpoint_auth_method": "client_secret_basic"
  }'

The response includes the issued client_id/client_secret and, when management is on, a registration_access_token and registration_client_uri to manage the client later.

Persistence & the admin console

Dynamically-registered clients are stored through the same OIDC adapter as the rest of the provider state, so they survive restarts. They do not appear in the admin console's client list (which reflects the static config); when dynamic registration is on, the admin clients page surfaces a notice to that effect.

On this page

Partner

Your product as the official integration for AuthKit, right here.

Become a partner →