Token endpoint
העתקת עמוד
POST /api/oauth/token — exchange an authorization code for tokens, or refresh them. Both grants, the request and response shapes, refresh-token rotation, and family revocation on replay.
עודכן
The token endpoint mints tokens. It handles two grants: authorization_code (exchange the code from /authorize) and refresh_token (rotate an expiring session). It is a machine endpoint — CSRF-exempt and CORS-open (*), because it carries client credentials and PKCE proofs, never cookies.
POST /api/oauth/token
Content-Type: application/x-www-form-urlencodedSend application/x-www-form-urlencoded (JSON is also accepted). Client authentication is client_secret_basic (HTTP Basic), client_secret_post (secret in the body), or none for public clients — where PKCE is the proof.
Grant: authorization_code
Exchange the one-time code for tokens.
Request
| Field | Required | Notes |
|---|---|---|
grant_type | ✓ | authorization_code |
code | ✓ | The code from the redirect (60-second TTL, single use). |
redirect_uri | ✓ | Must equal the authorize request’s redirect_uri byte-for-byte. |
code_verifier | ✓ | The PKCE verifier (43–128 chars) whose S256 hash was sent to /authorize. |
client_id | ✓ | Your client. |
client_secret | Confidential clients only (or use HTTP Basic). |
# --data-urlencode form-encodes each value safely (redirect URIs carry reserved characters)
curl -X POST https://entrybit.net/api/oauth/token \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=SplxlOBeZQQYbYS6WxSbIA" \
--data-urlencode "redirect_uri=entrybitresident://oauth/callback" \
--data-urlencode "client_id=eb_9f1c2ab34cd56ef7" \
--data-urlencode "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"Response
{
"access_token": "<RS256 JWT>",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email offline_access passes:read",
"refresh_token": "<opaque>",
"id_token": "<RS256 JWT>"
}Cache-Control: no-store is set on every token response. The id_token is present when openid was granted; the refresh_token is present when offline_access was granted.
Auth-code-injection defense: a replayed authorization code revokes the entire refresh-token family it originally minted. Never exchange a code twice.
Grant: refresh_token
Exchange the current refresh token for a fresh set — with a new refresh token.
Request
| Field | Required | Notes |
|---|---|---|
grant_type | ✓ | refresh_token |
refresh_token | ✓ | The current refresh token (they rotate on every use). |
client_id | ✓ | Your client. |
client_secret | Confidential clients only (or use HTTP Basic). | |
scope | Optional narrowing of the granted scope — never widening. |
# A refresh token is a live credential — keep it in your secret store, not the command line
curl -X POST https://entrybit.net/api/oauth/token \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=eb_9f1c2ab34cd56ef7"The response has the same shape as the authorization-code grant — including a brand-new refresh_token.
Refresh-token rotation (read this)
Refresh tokens are opaque, single-use, and rotate on every exchange. The rules that keep a session healthy:
- Always persist the newest
refresh_tokenreturned by each refresh. The old one is now dead. - Never keep two copies, and never refresh the same token twice concurrently. Presenting a superseded or revoked token is treated as theft: EntryBit revokes the entire token family (the stolen-token kill switch) and returns a generic
invalid_grant. - On
invalid_grant, clear your tokens and send the user to log in again.
In code, the contract is an ordering — persist the new refresh token before the new access token is used, and keep at most one refresh in flight:
// Sketch — a complete client (single-flight + invalid_grant handling) is in
// the React Native quickstart's lib/session.ts.
const tokens = await postTokenRefresh(stored.refreshToken); // ONE at a time, app-wide
await secureStore.set('refreshToken', tokens.refresh_token); // 1. the old token is dead
authorizeWith(tokens.access_token); // 2. only then use the new pairRefresh tokens live 60 days; access tokens live ~15 minutes. Access tokens are stateless JWTs — they simply expire and cannot be revoked individually (revoke the refresh token to end a session; see Revocation).
The tokens
| Token | Format | Lifetime | Claims / notes |
|---|---|---|---|
| access_token | RS256 JWT, typ: at+jwt (RFC 9068) | ~15 min | iss sub aud client_id scope iat nbf exp jti; aud = your client_id. |
| id_token | RS256 JWT, typ: JWT | — | iss sub aud iat exp + nonce? auth_time? amr? at_hash? + scope-gated identity. See ID token & UserInfo. |
| refresh_token | Opaque, SHA-256 stored | 60 days | Single-use, rotating. |
Both JWTs verify offline against the JWKS.
Errors
| Status | error | Meaning |
|---|---|---|
400 | invalid_grant | Code/refresh expired, reused, mismatched, or family-revoked. |
400 | invalid_request | A required parameter is missing or malformed. |
400 | invalid_scope | Requested a scope the client (or grant) doesn’t allow. |
400 | unsupported_grant_type | grant_type is not authorization_code or refresh_token. |
401 | invalid_client | Client authentication failed. |
429 | temporarily_unavailable | Rate limited — back off and retry. |
Descriptions are deliberately generic (no token-state oracle). Branch on the error code — see Errors.