Token requests
This page lists the parameters, responses, and errors for each flow on the OAuth server's token endpoint, <publicBaseURL>/oauth/token. Each flow follows the standard linked in its section. This page describes the values Defakto accepts and returns.
Common rules
- Requests are
POST, withContent-Type: application/x-www-form-urlencoded, and a body of at most 32 KiB. - Each parameter appears at most once.
- A request names the service its token is for with
resourceoraudience, not both. - Every request is decided by the trust domain's token policy, after the caller and any presented token are verified.
Client authentication
In token exchange requests, the workload authenticates as itself, in one of two ways. A request uses one of them.
| Method | Parameters | Notes |
|---|---|---|
| JWT-SVID client assertion | client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe and client_assertion=<JWT-SVID> | Works over any TLS connection, including one that ends at a load balancer in front of the Trust Domain Servers |
| X.509-SVID over mutual TLS | client_id=<SPIFFE ID>, and the X.509-SVID as the TLS client certificate | client_id must match the SPIFFE ID in the certificate (RFC 8705). The TLS connection must end at the Trust Domain Server, so this method isn't available behind a load balancer that doesn't pass the client certificate through. |
When a request has a client assertion, the client assertion is used, and any certificate on the connection is ignored. The SVID must belong to your trust domain.
In token exchange requests, a JWT-SVID client assertion must have its audience set to the trust domain's JWT issuer, not the OAuth issuer.
Workload access token
A workload exchanges its JWT-SVID for an access token, with the JWT bearer grant (RFC 7523). This is the flow MCP calls workload identity federation. See Call an MCP server from a workload.
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:jwt-bearer |
assertion | Yes | The workload's JWT-SVID, with its audience set to the OAuth issuer URL |
resource or audience | Yes | The service the token is for. It becomes the token's aud. |
scope | No | The requested scopes, separated by spaces. Without it, the access token has no scopes, and the response has no scope field. |
client_assertion_type and client_assertion | No | A JWT-SVID that authenticates the workload, with its audience set to the OAuth issuer URL. When present, the assertion's sub must be the same workload. |
The request can also carry a DPoP header, to bind the token to a key. See DPoP.
POST /oauth/token HTTP/1.1
Host: td-server.defakto.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<JWT-SVID>
&resource=https://helpdesk.example.com/mcp
&scope=tickets.read
{
"access_token": "<access token>",
"token_type": "Bearer",
"expires_in": 300,
"scope": "tickets.read"
}
The access token never outlives the JWT-SVID in assertion. See Lifetimes.
Without a client assertion, the assertion's signature, issuer, and audience are the only checks, so protect JWT-SVIDs as you would any bearer credential.
User sign-in
An OAuth client, such as an MCP client, gets an access token for a person with the authorization code grant and PKCE, as the MCP authorization specification describes. MCP clients implement this flow already. This section is for people building their own OAuth clients.
Defakto's values for the flow:
- Client ID. The client's
client_idis the URL of its Client ID Metadata Document. The client ID must be listed inoauthClients. Otherwise, the person's browser shows a Defakto error page, with HTTP status400and the referenceunknown-client, and nothing is sent back to the client. Theredirect_urimust be one that the document lists. - Authorization endpoint.
<publicBaseURL>/oauth/authorize. Defakto sends the person to the identity provider that haslogin, and the token policy decides the request when the person comes back. - Token endpoint.
<publicBaseURL>/oauth/token, withgrant_type=authorization_code. - Audience. Send
resourcewith the service the token is for, in both the authorization request and the token request. - PKCE. Required, with the
S256method only. A request without acode_challenge, or with theplainmethod, is refused. - Time limits. The person must finish signing in within 10 minutes of the authorization request. The authorization code is valid for 60 seconds, and can be used once.
- Access token. The access token lives for 5 minutes, unless the token policy sets a different lifetime, and never outlives the identity provider's ID token from the sign-in.
On-behalf-of token
A workload exchanges a user's token for an on-behalf-of token, with token exchange (RFC 8693). A workload that received an on-behalf-of token exchanges it again for the next service. See Propagate a user's identity.
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | The user's ID token or access token from an identity provider, or the on-behalf-of token to re-exchange |
subject_token_type | Yes | urn:ietf:params:oauth:token-type:id_token, urn:ietf:params:oauth:token-type:access_token, or, to re-exchange, urn:defakto:security:token-type:jwt-svid-obo |
requested_token_type | Yes | urn:defakto:security:token-type:jwt-svid-obo |
audience | Yes | The service the token is for |
| Client authentication | Yes | See Client authentication |
A presented ID token or access token must come from a configured identity provider. Its typ header must match the kind of token it's presented as, and a token from an identity provider is refused if its subject is a SPIFFE ID in your trust domain. A presented on-behalf-of token must be one that your trust domain issued.
{
"access_token": "<on-behalf-of token>",
"issued_token_type": "urn:defakto:security:token-type:jwt-svid-obo",
"token_type": "Bearer",
"expires_in": 300
}
The on-behalf-of token never outlives the subject_token. See Lifetimes.
DPoP isn't supported on token exchange. A request with a DPoP header is refused with invalid_dpop_proof. See DPoP.
Transaction Token
A workload exchanges a token for a Transaction Token, with token exchange.
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | The user's access token from an identity provider. ID tokens aren't accepted. |
subject_token_type | Yes | urn:ietf:params:oauth:token-type:access_token |
requested_token_type | Yes | urn:ietf:params:oauth:token-type:txn_token |
audience | Yes | Your trust domain name, such as example.com |
scope | Yes | The scopes of the transaction, separated by spaces. Each must be one of the scopes in the subject_token's scope or scp claim, so a subject_token without one of those claims is refused. |
| Client authentication | Yes | See Client authentication |
A Transaction Token can't be exchanged further.
{
"access_token": "<Transaction Token>",
"issued_token_type": "urn:ietf:params:oauth:token-type:txn_token",
"token_type": "N_A",
"expires_in": 300
}
The Transaction Token never outlives the subject_token. See Lifetimes.
The response follows the Transaction Tokens draft, so it never includes a refresh token.
External service token
A workload gets an access token for a configured external service, with token exchange. The workload acts as itself, so the request presents no subject token. See Get tokens for external services.
| Parameter | Required | Value |
|---|---|---|
grant_type | Yes | urn:ietf:params:oauth:grant-type:token-exchange |
audience or resource | Yes | The external service's resource value |
requested_token_type | No | urn:ietf:params:oauth:token-type:access_token, the default |
scope | No | The requested scopes, separated by spaces, within the service's configured grant.clientCredentials.scopes. Without it, Defakto requests all of them. |
| Client authentication | Yes | See Client authentication |
The response is a standard OAuth token response with access_token, token_type, and, when the service returns them, expires_in and scope. The access token is the external service's, unchanged. Other fields in the service's response aren't passed on.
Errors
Errors use the standard OAuth error response, a JSON body with error and error_description. The error_description gives a short reason, such as subject token rejected. Full details, such as which token policy rule refused the request, are recorded on the OAuth server's audit event for the request.
error | Common causes |
|---|---|
invalid_request | A required parameter is missing or repeated, both resource and audience are present, the body exceeds 32 KiB, or the token policy or the external authorizer refused a JWT bearer or token exchange request. In token exchange, also when the subject_token failed to verify, has expired, has the wrong issuer or audience, fails a claimValidationRules rule, or has a typ header for a different kind of token. The error_description is subject token rejected. |
invalid_client | The client assertion failed to verify, or client_id doesn't match the client certificate |
invalid_grant | In the JWT bearer grant, the JWT-SVID failed to verify, has expired, or has the wrong audience, or its sub doesn't match the authenticated client. Or the token policy refused the request when the client redeemed an authorization code. |
invalid_scope | None of the requested scopes is allowed |
invalid_dpop_proof | The DPoP proof is invalid, or was sent on token exchange. See DPoP. |
unsupported_grant_type | The Trust Domain Server doesn't support the grant |
server_error | The OAuth server couldn't complete the request. For example, an identity provider's keys couldn't be fetched, the external authorizer was unavailable or returned an invalid response, or an external service's authorization server couldn't issue a token |
When the token policy refuses a person's sign-in, the OAuth client receives access_denied at its redirect URI, as the authorization code grant defines.