veil

Exchange a code or a refresh token

The token endpoint: the code with its PKCE verifier, or a refresh token, for an access token, an ID token and a new refresh token.

POST/oidc/token
Access
Private code required
Client
Local or server client

JSON body#

NameLocationMeaning
grant_typeRequiredJSONauthorization_code or refresh_token.
codeJSONThe code (authorization_code).
redirect_uriJSONThe same as in the request (authorization_code).
code_verifierJSONThe PKCE verifier, 43 to 128 characters (authorization_code).
refresh_tokenJSONThe refresh token (refresh_token).
scopeJSONA refresh may narrow the scopes.
client_idJSONA public app names itself here.
client_assertion, client_assertion_typeJSONprivate_key_jwt (RFC 7523): a JWT signed by a key of the app's jwks, aud the issuer or this endpoint, exp within 5 minutes, a new jti.

Request examples#

cURL
curl -fsS -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER" \
  --data-urlencode redirect_uri=https://app.example.org/callback "https://veil.rip/oidc/token"

Response#

HTTP 200application/json

{access_token, token_type: Bearer, expires_in: 600, refresh_token?, id_token, scope}.

FieldTypeMeaning
access_tokenstringOpaque, veil_at_…, 10 minutes. Kept only as a hash.
token_typestringBearer.
expires_ininteger600.
refresh_tokenstringveil_rt_…, 30 days, rotated at every use, at most 90 days from the login. Missing for an app without refresh.
id_tokenstringJWT signed with the app's algorithm (EdDSA or ES256), kid in the header.
scopestringThe scopes granted.

Response headers#

HeaderValueMeaning
Content-Typeapplication/jsonJSON, also for errors.
Cache-Controlno-storePrivate: never cached.
Pragmano-cacheRFC 6749, 5.1.

What to verify#

In the ID token check the signature with the JWKS key its kid names, iss, aud and azp (your client_id), exp, and nonce. With the veil scope, veil_consent can be checked too: see the guide.

Usage notes#

  • Errors of this endpoint follow OAuth: {"error", "error_description"} (RFC 6749, 5.2), not the error object of the rest of the API. A rate limit still answers 429 with the usual envelope.
  • application/x-www-form-urlencoded, at most 16 KiB, each parameter once. A confidential app uses Basic authentication.
  • No API key is read here: an Authorization header on /oidc/ is the app's or the person's own credential.

Errors#

API errors return an error object. Use error.code for program logic; error.message is English text for people and can change. Response and error guide

HTTPCodeMeaning and next step
400invalid_grant

OAuth error: the code or refresh token is invalid, expired, used, revoked or another app's, or the PKCE verifier does not match. A code or refresh token used twice ends every token of its login.

Start the sign-in again. Never retry a code.

400invalid_request

OAuth error: a parameter is missing, given twice or malformed, or the body is not a form.

Read error_description, fix the request and send it again.

400invalid_scope

OAuth error: a refresh asked for a scope the login did not grant, or left out openid.

Keep or narrow the scopes, with openid. A new scope needs a new sign-in.

400unsupported_grant_type

OAuth error: grant_type is not authorization_code or refresh_token.

Use one of the two grant types.

401invalid_client

OAuth error: the app could not be authenticated, with the method it registered.

Check the client id and secret (Basic), the client assertion (private_key_jwt, a new jti each time) or client_id for a public app.

404invalid_request

OAuth error: Sign in with Veil is not configured on this server.

Use https://veil.rip, or a server whose discovery document answers.

Shared request errors
HTTPCodeMeaning and next step
429rate_limited

An anonymous, signed-request or API-key allowance was exceeded.

Wait at least Retry-After seconds. Share a queue and retry budget across your workers.

500internal_error

An unexpected server error occurred. Internal details are not returned.

Retry reads with bounded backoff. Check write results before sending a new signed operation.

503busy

The application has reached a temporary concurrency or database limit.

Wait Retry-After seconds and retry with bounded backoff.