Whether you’re building a massive SaaS platform or a niche API, OAuth is the gold standard for authorization. In such there might arise a hurdle that every developer faces: Registration.
In general, every OAuth client needs to be manually registered with an authorization server before it can request tokens. Usually, this means an admin opens a portal, types in a name and redirect URI, copies the client_id and client_secret into a config file, and hits deploy. For a few internal apps, that’s works. But that manual flow breaks completely when you want to register thousands of clients. This whre Dynamic Client Registration (DCR) enters.
The Specs: RFC 7591 and 7592
To solve the scaling problem, we have two key standards:
- RFC 7591: Defines a protocol for clients to register themselves at runtime by posting JSON metadata to a registration endpoint.
- RFC 7592: Extends the protocol with management API to read, update, and delete those registrations.
In this post, we’ll dive into how these specs work and walk through working Ruby implementation I have built on Roda and rodauth-oauth.
The Problem with Static Client Registration
Why do we need a new way to register? Static registration suffers from three structural flaws:
- The Human in the Loop: A person has to manually provision every client. In a multi-tenant environment, this forces you to either give every tenant admin access (a massive security risk) or build a custom “wrapper” API for a process that wasn’t meant to be automated.
- No Lifecycle Standard: Every provider has a different UI or proprietary API. If your client needs to rotate its secret, update its redirect URIs, or delete itself when a customer churns, you’re stuck writing ugly code for every single provider.
- Zero Verification: Credentials go into a config file and sit there forever. There is no cryptographic link between the software itself and the registration record.
DCR addresses all three by turning registration into a standardized, machine-to-machine conversation.
RFC 7591: The Registration Protocol
The entire protocol is one HTTP endpoint: POST /register.
The client sends a JSON body with its metadata, and the server responds with client_id and client_secret (if applicable). The response code is 201 Created, not 200 OK.
POST /register HTTP/1.1
Host: auth.example.com
Content-Type: application/json
Authorization: Bearer <initial_access_token>
{
"client_name": "Billing Service",
"redirect_uris": ["https://billing.example.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "read write"
}
And after successful response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"client_id": "s6BhdRkqt3",
"client_secret": "cf136dc3c1fd86244b075d2e....",
"client_secret_expires_at": 0,
"client_name": "Billing Service",
"redirect_uris": ["https://billing.example.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "read write",
"registration_client_uri": "https://auth.example.com/register/s6BhdRkqt3",
"registration_access_token": "this.is.the.management.token"
}
A few things to note here.
- A
client_secret_expires_atvalue of0means the secret is permanent. Any other value is a standard Unix timestamp. - The
registration_client_uriandregistration_access_tokenare only returned if the server supports RFC 7592 (the management spec). These are your keys to manage the client later. - The
Authorization: Bearerheader contains the initial access token. This is pre-shared credential that determines who is allowed to register. Without this, your endpoint is open to entire internet—and you’ll be drowning in “garbage” registrations and DOS attacks. - DCR introduces
software_idandsoftware_versionto identify the “version” of the app, distinct from the specific instance (client_id). For high-security environments,software_statement(a signed JWT) allows the server to cryptographically verify exactly what software is knocking at the door.
Error responses
On failure, the server returns 400 Bad Request with one of four error codes:
invalid_redirect_uri: one of the submitted redirect URIs is malformed or disallowed.invalid_client_metadata: a metadata field has an unacceptable value.invalid_software_statement: the JWT insoftware_statementis malformed.unapproved_software_statement: the software statement issuer is not trusted.
RFC 7592: The Management Protocol
RFC 7592 defines what happens after registration. The registration response includes a registration_client_uri (a URL unique to the client) and a registration_access_token. Using those, the client can manage its own registration without involving an admin.
Three operations are supported on the client URI:
Read (GET)
GET /register/s6BhdRkqt3 HTTP/1.1
Authorization: Bearer this.is.the.management.token
Returns the current client metadata.
note: The server may rotate the
registration_access_tokenon every read response, if it does (and returns key in the response), the old token must be discarded.
Update (PUT)
PUT /register/s6BhdRkqt3 HTTP/1.1
Authorization: Bearer this.is.the.management.token
Content-Type: application/json
{
"client_id": "s6BhdRkqt3",
"client_name": "Billing Service v2",
"redirect_uris": ["https://billing.example.com/callback", "https://billing.example.com/callback2"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "read write"
}
PUT is a full replacement, not a patch. The spec is explicit that: “valid values of client metadata fields in this request MUST replace, not augment, the values previously associated with this client.” If you omit redirect_uris, they’re gone so send all fields every time.
The response send back the current metadata. If the server rotated the registration_access_token, a new one appears in the response. The old one is invalid from that point forward.
You cannot include registration_access_token, registration_client_uri, or client_secret_expires_at in the PUT body. The server ignores/rejects them.
Delete (DELETE)
DELETE /register/s6BhdRkqt3 HTTP/1.1
Authorization: Bearer this.is.the.management.token
Successful deletion returns 204 No Content. After deletion, any subsequent request to the same registration_client_uri returns 401 Unauthorized, not 404. The endpoint still exists, it’s the authentication that fails because the client no longer exists.
note: RFC 7592 is entirely optional. A server that implements RFC 7591 is not restricted to support management operations. Whether the management API is available can be found by presence (or absence) of
registration_client_uriin the registration response.
The Full Lifecycle
sequenceDiagram
participant C as Client
participant AS as Authorization Server
C->>AS: POST /register (with initial_access_token)
AS-->>C: 201 Created (client_id, client_secret, registration_access_token)
Note over C,AS: Client uses tokens normally
C->>AS: GET /register/:client_id (registration_access_token)
AS-->>C: 200 OK (current metadata)
C->>AS: PUT /register/:client_id (all fields + registration_access_token)
AS-->>C: 200 OK (updated metadata, possibly new registration_access_token)
C->>AS: DELETE /register/:client_id (registration_access_token)
AS-->>C: 204 No Content
Each step requires the registration_access_token issued at registration. If the server rotates it on GET or PUT, treat the new token as the only valid one immediately.
Security Considerations
Protect the registration endpoint. An open POST /register is a gift to attackers. They can flood your DB with junk clients or use your server to launch phishing attacks. Always require an initial access token in production.
Validate URI fields before fetching. Fields like jwks_uri or logo_uri tell your server to go fetch something from the web. If you don’t validate these URLs, an attacker can point them at your internal metadata services (like 169.254.169.254). Always allowlist hostnames.
An open registration endpoint weakens your consent model. Because DCR gives anyone a legitimate client_id, it can be used to make phishing sites look “official.” Consider restricting the scopes available to dynamic clients until an admin flags them as “trusted.”
When to Use Dynamic Client Registration
DCR fits well in these scenarios:
- Multi-tenant SaaS: Each tenant’s integration registers its own client at onboarding time. No manual admin steps; no shared credentials across tenants.
- Mobile or third-party SDKs: Every installation of your SDK registers a unique client, giving you per-instance visibility and the ability to revoke individual instances.
- Federated environments: A client needs to register with multiple authorization servers (different regions, different organizations). DCR gives you one standard flow regardless of AS vendor.
- API gateway self-registration: Services in a microservices mesh register their own clients on startup and deregister on shutdown.
When to skip it? If you only have limited no. of internal apps that rarely change, static registration is simpler, easier to audit, and carries less overhead.
AI Agents and MCP
DCR may also be the right fit for AI agent systems. As agents use protocols like Model Context Protocol (MCP) to interact with tools and APIs, manually provisioning a client_id for every sub-agent spawned at runtime isn’t practical. Dynamic registration lets an agent register its own instance on startup, request only the scopes it needs for that session, and clean up its credentials via RFC 7592 when the task is done.
OIDC Dynamic Registration
OpenID Connect Dynamic Client Registration 1.0 is built on the same pattern as RFC 7591, but it’s a parallel spec, not a strict superset. It adds OIDC-specific fields (id_token_signed_response_alg, subject_type, default_max_age, and others), mandates TLS on the registration endpoint, and strictly couples registration_client_uri with registration_access_token in the response. The endpoint is found via registration_endpoint in .well-known/openid-configuration.
Hands-On: A Ruby Implementation
To see this in action, I’ve put together a sample application: roda-oauth-dynamic-registration. It implements both RFCs using the Roda web toolkit and rodauth-oauth in just about 100 lines of code.
If you want to get your hands dirty and test the registration flow yourself, you can check out the repository and follow the setup guide in the README.
Conclusion
The two RFCs split cleary 7591 owns the initial registration, 7592 owns everything after. A server can implement just 7591 and skip the management layer entirely, but if it does, clients have no standard way to update or deregister themselves. Most real deployments need both.
