Skip to main content

Token API

POST /v1/token/exchange​

RFC 8693 token exchange — form-encoded, not JSON, and its errors follow RFC 6749 §5.2 ({"error": "...", "error_description": "..."}) rather than this project's usual error envelope, since an OAuth client already knows how to read that shape.

Request (application/x-www-form-urlencoded):

FieldRequiredNotes
grant_typeYesMust be urn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYesThe presented (current-hop) token
subject_token_typeYesMust be urn:ietf:params:oauth:token-type:jwt
resourceYesThe target this exchange narrows to
scopeYesThe data class this exchange narrows to
action_idYesThe action_id from the POST /v1/authorize allow this exchange spends
actor_token / actor_token_typeNoProof-of-possession actor assertion (future identity work)
audienceNo
taintNo

An exchange re-decides rather than trusting that /v1/authorize was called first, and it only succeeds if action_id names a committed allow row matching this exact hop (session, caller, target, data class) — see The delegation-chain audit model.

Response — 200:

{"access_token": "<JWT, hop N+1>", "issued_token_type": "urn:ietf:params:oauth:token-type:jwt", "token_type": "Bearer", "expires_in": 60}

Decoded, the new token's sub is unchanged, aud is exactly resource, scope is exactly the requested scope, hop is one greater than the subject token's, and act nests the subject token's own sub — the delegation proof.

Errors (RFC 6749 shape, all in the error/error_description form above):

HTTPerrorMeaning
403invalid_targetThe recorded allow no longer holds under a fresh decision
400invalid_grant (already exchanged)subject_token's jti was already spent
400invalid_grant (bad action_id)Missing or malformed action_id
503invalid_grant (guard unavailable)Single-use replay guard couldn't be reached; refuses to issue rather than risk a double-mint

See JWKS for the companion endpoint a resource server uses to verify the tokens this endpoint mints, offline.