Registering a client dynamically
The printer became photo-printer when a developer at the printing company filled in a form in the photo service's developer console. Client IDs and registration noted that a form is not the only way to create a registration, and that some services let software register itself through an API.
A form suits a client that knows in advance which authorization servers it will use. The printer now follows APIs to authorization servers it has never met, and no developer can fill in a form for a server that was discovered a moment ago.
When a form is not enough
Discovery is one reason to register in code. There are others closer to home.
The printer's phone app has a single client ID, photo-printer-app, shared by every installation. The photo service cannot tell one phone's requests from another's, cannot give any installation a credential of its own, and cannot switch off one misbehaving copy without switching off all of them.
The printing company also creates a temporary copy of its test website for each change under review, at addresses such as https://review-482.test.printer.example. Each copy needs its own client with its own redirect URI for a few days, and nobody wants to visit a console several times a day to create and delete them.
The OAuth 2.0 Dynamic Client Registration Protocol, published as RFC 7591, turns registration into an HTTP request. The authorization server offers a registration endpoint, and a client finds its address in the server's metadata as registration_endpoint. For the photo service it is https://auth.photos.example/register.
Sending a registration request
All domains, identifiers, keys, and tokens in these examples are fictional. When the phone app starts for the first time, the installation generates a key pair in the phone's secure hardware and registers itself:
POST /register HTTP/1.1
Host: auth.photos.example
Content-Type: application/json
Accept: application/json
{
"client_name": "Photo Printer",
"redirect_uris": ["https://app.printer.example/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "private_key_jwt",
"jwks": {
"keys": [
{ "kty": "EC", "crv": "P-256", "kid": "install-key-1", "x": "...", "y": "..." }
]
},
"scope": "photos.read"
}
The key values are shortened here. Unlike a token request, the body is JSON, and its members are the client metadata fields from Redirect URIs and client metadata. The installation describes itself the way a developer would in a console form: its name, its return address, the grants it will use, and how it will authenticate. An app on a phone cannot publish a key set at a web address, so it sends its public key by value in jwks instead of registering a jwks_uri. The private key never leaves the phone's hardware.
If the photo service accepts the request, it answers with 201 Created and a JSON body that contains a new client ID for this installation, app-7c41e2b9, along with the metadata it registered. From then on, this installation sends client_id=app-7c41e2b9 in its authorization requests and signs client assertions with its own key at the token endpoint.
Open and protected registration
The request above carried no credential at all. That is open registration: anyone who can reach the endpoint can create a client. RFC 7591 recommends that registration endpoints accept requests without a credential, to make open registration possible, and lets servers limit how many they accept.
Open registration has a cost the photo service has to plan for. "Anyone" includes an attacker, who can register a client called Photo Printer, with the printing company's logo and a redirect URI on their own site. The server must treat everything in an open registration as the registrant's own claim. It can label dynamically registered clients on its consent screens, warn about clients that are new or that few people have approved, and limit what such clients may request. Current security guidance describes attackers using dynamic registration in exactly this way, to obtain a legitimate-looking client for phishing or for a mix-up attack.
Protected registration requires an initial access token: an OAuth access token, usually sent as a bearer token, that authorizes calls to the registration endpoint. How it is issued is left to the service. A common pattern is a developer console that issues one to a developer, which ties every client registered with it back to that developer. The printing company's deployment pipeline holds one in its secrets store and uses it to register each review site:
POST /register HTTP/1.1
Host: auth.photos.example
Authorization: Bearer demo-initial-access-token-5
Content-Type: application/json
Accept: application/json
{
"client_name": "Photo Printer review 482",
"redirect_uris": ["https://review-482.test.printer.example/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "photos.read"
}
The photo service now knows who is asking, and it can apply rules attached to that token, for example that its clients may use only redirect URIs under test.printer.example and may connect only test accounts. If the token leaks, revoking it stops new registrations made with it. The clients it already created are separate records, which the photo service can review on their own.
One client per installation
Back on the phone, per-installation registration changes what the photo service can see and do. Each installation has its own client ID and its own key. A refresh token issued to app-7c41e2b9 is useless to any other installation, because the token endpoint expects a client assertion signed with that installation's private key. If one phone is lost or one copy misbehaves, the photo service can disable that client and leave every other installation working.
Guidance for native apps classifies them as public clients, with one exception: an app that uses a mechanism such as dynamic registration to give each installation its own credential. Be precise about what that credential proves, though. A valid client assertion for app-7c41e2b9 shows that the request comes from whoever holds the private key registered for that client, and because the key never leaves the phone's secure hardware, that means the installation that registered. It does not show that the installation is a genuine copy of Photo Printer. Anyone can send the same registration request from a modified app or a short script and receive a client ID and credential of their own. The credential proves continuity with one registration, not which software made it.
So the photo service still treats the app's identity with the caution it gives a public client. It asks for your approval when a new installation first connects, even if you approved Photo Printer on another phone, and it does not let a name or logo stand in for proof. It relies on exact redirect URI matching and still expects PKCE. Evidence about the software itself has to come from somewhere else, such as platform attestation: a signed statement from the phone's platform that an unmodified copy of a particular app is running on a genuine device.