Dynamic Client Registration

Authagonal implements OAuth 2.0 Dynamic Client Registration (RFC 7591), allowing client applications to register themselves at runtime without administrator involvement.

Enabling the Endpoint

Dynamic registration is disabled by default. Opt in via configuration:

{
  "Auth": {
    "DynamicClientRegistrationEnabled": true
  }
}

Or set Auth__DynamicClientRegistrationEnabled=true as an environment variable. A multi-tenant host can override the setting per tenant through ITenantContext.DynamicClientRegistrationEnabled: a tenant’s own answer wins, and null falls back to the host-wide option.

When enabled, the discovery document advertises the endpoint:

GET /.well-known/openid-configuration
{
  "registration_endpoint": "https://auth.example.com/connect/register"
}

Registering a Client

POST /connect/register
Content-Type: application/json

{
  "client_name": "My App",
  "redirect_uris": ["https://myapp.example.com/callback"],
  "post_logout_redirect_uris": ["https://myapp.example.com/"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "client_secret_basic",
  "scope": "openid profile email offline_access",
  "audiences": ["https://api.myapp.example.com"],
  "allowed_cors_origins": ["https://myapp.example.com"],
  "backchannel_logout_uri": "https://myapp.example.com/oidc/backchannel",
  "frontchannel_logout_uri": "https://myapp.example.com/oidc/frontchannel",
  "frontchannel_logout_session_required": true
}

Response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "client_id": "a1b2c3d4e5f6...",
  "client_secret": "xkCd2_base64url...",
  "client_id_issued_at": 1745000000,
  "client_secret_expires_at": 0,
  "client_name": "My App",
  "redirect_uris": ["https://myapp.example.com/callback"],
  "post_logout_redirect_uris": ["https://myapp.example.com/"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "openid profile email offline_access",
  "token_endpoint_auth_method": "client_secret_basic"
}

The client_secret is returned once and cannot be retrieved later. Store it securely. The response is sent with Cache-Control: no-store. client_id is 32 lowercase hex characters, and client_secret_expires_at is always 0 (secrets do not expire). Public clients (none) and private_key_jwt clients get no client_secret in the response. The response echoes only the fields shown: audiences, jwks, jwks_uri, allowed_cors_origins and the logout fields are stored but not returned.

Request Parameters

Parameter Required Notes
client_name no Defaults to the generated client_id if omitted
redirect_uris conditional Required when grant_types contains authorization_code. Must be absolute URIs; javascript:/data:/vbscript:/file: schemes are rejected (native custom schemes for mobile deep links are fine). A fragment is rejected (RFC 6749 §3.1.2), and cleartext http is accepted only for loopback hosts (RFC 8252 §7.3). At most 20 entries, each at most 2048 characters.
post_logout_redirect_uris no Valid redirect targets after logout. Same 20-entry / 2048-character bounds as redirect_uris.
grant_types no Defaults to ["authorization_code"]. Only authorization_code and refresh_token are registrable: client_credentials, implicit, device and any other grant type are rejected with invalid_client_metadata, so open registration can never mint a machine-to-machine client. refresh_token is added automatically if offline_access is requested.
token_endpoint_auth_method no client_secret_basic (default), client_secret_post, private_key_jwt, or none for public clients. Any other value is refused with invalid_client_metadata.
jwks / jwks_uri with private_key_jwt The client’s public keys. One of the two is required for private_key_jwt (otherwise invalid_client_metadata); jwks_uri must pass the outbound-URL guard (an external address). A private_key_jwt client is issued no secret.
scope no Space-separated scopes. Only the five OIDC built-ins (openid, profile, email, phone, offline_access) plus whatever Auth:DynamicClientRegistrationScopes names are registrable: existence in the scope store is not enough (see Scopes). Role-gated scopes and the administrative scope (AdminApi:Scope, default authagonal-admin) can never be registered.
audiences no JWT aud values added to access tokens. At most 20 entries of at most 512 characters, each an absolute URI without a fragment; a bad value is invalid_client_metadata.
allowed_cors_origins no Each entry must be a valid origin (otherwise invalid_client_metadata), but the value is not stored as sent: the client’s allowed origins are derived from the origins of its own https redirect_uris, so a registrant can only reach origins it already proved a redirect URI for.
backchannel_logout_uri no Enables Back-Channel Logout
frontchannel_logout_uri no Enables Front-Channel Logout
frontchannel_logout_session_required no Defaults to true; when true, the logout URL carries iss and sid parameters

Defaults & Invariants

Error Responses

HTTP error Cause
400 invalid_redirect_uri One of redirect_uris is not a valid absolute URI, uses a script/data/file pseudo-scheme, carries a fragment, is cleartext http to a non-loopback host, or (for either URI list) is longer than 2048 characters
400 invalid_client_metadata A non-registrable grant type was requested, redirect_uris is missing for a grant type that requires it, token_endpoint_auth_method is unsupported, private_key_jwt has no jwks/jwks_uri (or an unsafe jwks_uri), audiences is invalid, an allowed_cors_origins entry is not an origin, or a logout URI is not an external address
400 invalid_scope A requested scope is neither built-in nor registered
400 invalid_client_metadata More than 20 redirect_uris / post_logout_redirect_uris
403 invalid_scope A requested scope is not registrable: not in Auth:DynamicClientRegistrationScopes, or role-gated
403 invalid_scope The administrative scope was requested, it can never be granted through registration
403 invalid_scope A registered IClientScopeGuard refused a requested scope (the anonymous caller is passed to it)
403 not_supported Dynamic client registration is not enabled
429 rate_limited Too many registrations from this IP (10 per hour)

Security Considerations

The registration endpoint is unauthenticated, but constrained by design:

What it does not constrain, unless the registrant opts in, is the audience. RFC 7591 has no field for one, so a stock registration omits audiences (an Authagonal extension) entirely: the client was never asked, its list is “unset”, and it may name any absolute URI as its resource at the authorization endpoint and receive a token carrying that value as aud. That is deliberate (the MCP authorization spec requires clients to name the MCP server as the resource, and an MCP client is a DCR client), and it makes the resource server responsible for authorizing on scope rather than on iss + aud + sub. Sending audiences, even as an empty list, is an answer and pins the client to it: a non-empty list is the allowlist for resource, and an explicit [] means the client may not name a resource at all. Token exchange is the exception: there an unset Audiences denies outright, so a registered client cannot aim an exchanged token anywhere. See Audiences and resource indicators.

For stronger gating (initial access tokens, mTLS, software statements), front the endpoint with your own middleware or an IAuthHook. Consider disabling dynamic registration entirely and managing clients via the admin API in environments where self-service registration is not a requirement.