Skip to main content

GET /registration-tokens, POST /registration-tokens and…

GET /registration-tokens, POST /registration-tokens and DELETE /registration-tokens/:id​

All three require manage-clients, the same capability the client routes above need — an initial access token is configuration for dynamic registration, not a population of its own. POST mints one: {"uses": <int, 1–2147483647>, "ttl_seconds": <int, 60–31536000>} — a year is this project's own ceiling on how long a bootstrap credential may outlive the operator who minted it, not a protocol or storage limit — answering 201 with id, token, remaining_uses and expires_at. This is the only response, from any route, that ever carries token — a following GET lists id, remaining_uses, created_at and expires_at and nothing else, the plaintext is never logged, and it never appears in an audit detail. DELETE revokes one by id, answering 204, or 404 if the id names no live token — one already spent to zero uses or expired answers 404 the same way an id nothing ever minted does, since GET was already hiding it from the caller.

GET never lists a token that has been spent to zero uses or has expired — the same two conditions spend already refuses under, so the list a caller sees is exactly the set of tokens that would still redeem. It pages in SQL, like every other listing here: a page anchored on a token that has since expired, been spent out or been revoked still resumes from the same place, because the cursor is a plain id comparison rather than a position inside a snapshot of the whole set. Minting records uses and ttl_seconds in the audit trail; revoking records what was revoked (remaining_uses, expires_at) the same way a client's own amendment does, through the same allowlisted diff — never the token or its hash, on either action.

demo's own policy is disabled; this run opens it to token first, the same door PATCH /settings opens to open for the sections above, and closes it again at the end, in the same transcript:

curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"client_registration_policy": "token"}' \
http://localhost:3000/admin/tenants/demo/settings > /dev/null

curl -sS -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uses": 1, "ttl_seconds": 3600}' \
http://localhost:3000/admin/tenants/demo/registration-tokens
{"id":"01a0e1bd-1658-7dd7-a886-e6d701026191","token":"iMpoXXUt6YF1ioil8i0eI8b912sLAHPjNLISdtXG1Eo","remaining_uses":1,"expires_at":"2026-09-27T08:21:00.760Z"}

Listing right after shows the same token with no token field, and presenting it at /tenants/demo/clients-registrations/openid-connect registers a client:

curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3000/admin/tenants/demo/registration-tokens
curl -sS -X POST \
-H "Authorization: Bearer iMpoXXUt6YF1ioil8i0eI8b912sLAHPjNLISdtXG1Eo" \
-H "Content-Type: application/json" \
-d '{"redirect_uris": ["https://rp.example/cb"]}' \
http://localhost:3000/tenants/demo/clients-registrations/openid-connect
{"items":[{"id":"01a0e1bd-1658-7dd7-a886-e6d701026191","remaining_uses":1,"created_at":"2026-09-27T07:21:00.759Z","expires_at":"2026-09-27T08:21:00.760Z"}]}
{"client_id":"01a0e1bd-2763-78a3-bf18-de64780558d8","client_id_issued_at":1790493665,"client_secret":"BvxM_Jnw2jU3gWyhMo-1tOVdAIJoOaBdqTdT7TCXTmM","client_secret_expires_at":0,"redirect_uris":["https://rp.example/cb"],"grant_types":["authorization_code"],"token_endpoint_auth_method":"client_secret_basic"}

A second, freshly minted token demonstrates the other half — DELETE, then a registration attempt with the now-revoked token:

curl -sS -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uses": 1, "ttl_seconds": 3600}' \
http://localhost:3000/admin/tenants/demo/registration-tokens
{"id":"01a0e1bd-36f3-7e51-84a5-25d4b0b2d50e","token":"CCsWyDsal7TdxscIypKk4s0kG7Id6fsTTD2JW719oxU","remaining_uses":1,"expires_at":"2026-09-27T08:21:09.108Z"}
curl -sS -D - -o /dev/null -X DELETE \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3000/admin/tenants/demo/registration-tokens/01a0e1bd-36f3-7e51-84a5-25d4b0b2d50e \
| grep -iE '^HTTP'
curl -sS -D - -o /dev/null -X POST \
-H "Authorization: Bearer CCsWyDsal7TdxscIypKk4s0kG7Id6fsTTD2JW719oxU" \
-H "Content-Type: application/json" \
-d '{"redirect_uris": ["https://rp.example/cb"]}' \
http://localhost:3000/tenants/demo/clients-registrations/openid-connect \
| grep -iE '^HTTP|www-authenticate'
HTTP/1.1 204 No Content
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="client-registration", error="invalid_token"

The audit trail for that mint and that revoke, each scoped to the second token's own resource_id so this prints only the row it describes rather than every mint or revoke demo has ever recorded:

With actor_name and actor_origin on each row, after a token is minted in its demo and revoked the same way, the id is a fresh run's, not the one above:

curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3082/admin/tenants/demo/audit?action=registration_token.mint&resource_type=registration_token&resource_id=01a0ee8f-0361-755d-b510-c9d6fde88a03"; echo
curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3082/admin/tenants/demo/audit?action=registration_token.revoke&resource_type=registration_token&resource_id=01a0ee8f-0361-755d-b510-c9d6fde88a03"; echo
{"items":[{"id":"01a0ee8f-0362-7553-addc-3f32813a6ac1","occurred_at":"2026-09-29T19:05:45.056Z","event_type":"admin_mutation","action":"registration_token.mint","outcome":"allowed","actor_tenant_id":"0199aa00-0000-7000-8000-000000000001","actor_subject_id":"01a0ee8a-bfb8-763c-ac22-0c8d97b0fada","actor_client_id":"01a0ee8a-bf72-77c9-a423-f61084924d9f","actor_name":null,"actor_origin":"system","resource_type":"registration_token","resource_id":"01a0ee8f-0361-755d-b510-c9d6fde88a03","request_id":"01a0ee8f-0356-773a-bb66-91367ea0bff9","ip":"172.22.0.1","detail":{"uses":1,"ttl_seconds":3600}}]}
{"items":[{"id":"01a0ee8f-036e-7d81-bda1-1a6bc375bfde","occurred_at":"2026-09-29T19:05:45.068Z","event_type":"admin_mutation","action":"registration_token.revoke","outcome":"allowed","actor_tenant_id":"0199aa00-0000-7000-8000-000000000001","actor_subject_id":"01a0ee8a-bfb8-763c-ac22-0c8d97b0fada","actor_client_id":"01a0ee8a-bf72-77c9-a423-f61084924d9f","actor_name":null,"actor_origin":"system","resource_type":"registration_token","resource_id":"01a0ee8f-0361-755d-b510-c9d6fde88a03","request_id":"01a0ee8f-0366-718b-a606-cbb01d9513c9","ip":"172.22.0.1","detail":{"expires_at":{"before":"2026-09-29T20:05:45.057Z"},"remaining_uses":{"before":1}}}]}
curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"client_registration_policy": "disabled"}' \
http://localhost:3000/admin/tenants/demo/settings | head -c 60
{"display_name":null,"enabled":true,"registration_allo