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
defineConfig({
// ...
dynamicRegistration: {
enabled: true,
initialAccessToken: env.get('AUTHKIT_REGISTRATION_IAT'), // strongly recommended
management: true, // RFC 7592 read/update/delete
},
})| Field | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | false | Exposes the registration endpoint (oidc-provider's registration feature). |
initialAccessToken | string? | — | Requires this Initial Access Token (IAT) as a bearer to register (RFC 7591 §3). When absent, registration is open. |
management | boolean | false | Enables Registration Management (RFC 7592): read/update/delete a registered client via its registration_access_token. |
redirectUriPolicy | RedirectUriPolicy | false | open registration: { loopback: true }; with an IAT: none | Where 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:
- every
redirect_uris/post_logout_redirect_urisentry must match the policy; - the client is restricted to the code flow:
grant_types⊆authorization_code,refresh_tokenandresponse_types=['code'](PKCE is already mandatory); - a client whose redirects are all loopback / private-use schemes is registered as
application_type: 'native'(unless it says otherwise); validateRegistrationruns, if configured.
A rejected registration gets 400 { "error": "invalid_redirect_uri" | "invalid_client_metadata" }.
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)
},
},| Field | Type | Default | Notes |
|---|---|---|---|
loopback | boolean | true | Plain-http loopback hosts on any port. Embedded credentials and fragments are always rejected. |
exact | string[] | [] | Redirects accepted by exact string equality. |
appSchemes | string[] | [] | Private-use schemes, without :. |
anyHttps | boolean | false | Accept 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.