Token Endpoint
The token endpoint exchanges a valid authorization code for an ID token and optionally a refresh token. It also handles refresh token rotation.
POST https://api.yorauth.com/oidc/token
Client secrets are server-side credentials. The client_secret parameter must never be included in client-side code. Token exchange requests must be made from your server. For public clients (SPAs without a backend), use PKCE instead. See Security Best Practices.
The endpoint is rate-limited to 60 requests per minute. Credentials are sent as form-encoded body parameters — HTTP Basic Auth is not supported.
Authorization Code Grant
Exchange a one-time authorization code (received from the Authorization Flow) for tokens.
Request
POST /oidc/token
Content-Type: application/x-www-form-urlencoded
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
code | Yes | The authorization code from the redirect |
redirect_uri | Yes | Must exactly match the redirect_uri used in the authorization request |
client_id | Yes | Your client's client_id |
client_secret | Yes | Your client's secret |
code_verifier | Yes | The PKCE code_verifier generated before the authorization request |
curl -X POST https://api.yorauth.com/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=the-code-from-redirect" \
-d "redirect_uri=https://yourapp.com/auth/callback" \
-d "client_id=oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345" \
-d "client_secret=your-client-secret" \
-d "code_verifier=your-original-code-verifier"
const response = await fetch('https://api.yorauth.com/oidc/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: codeFromRedirect,
redirect_uri: 'https://yourapp.com/auth/callback',
client_id: 'oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345',
client_secret: 'your-client-secret',
code_verifier: sessionStorage.getItem('pkce_code_verifier'),
}),
});
const tokens = await response.json();
$response = Http::asForm()->post('https://api.yorauth.com/oidc/token', [
'grant_type' => 'authorization_code',
'code' => $codeFromRedirect,
'redirect_uri' => 'https://yourapp.com/auth/callback',
'client_id' => 'oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345',
'client_secret' => config('services.yorauth.client_secret'),
'code_verifier' => session('pkce_code_verifier'),
]);
$tokens = $response->json();
Successful Response
{
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InlvcmF1dGgta2V5LTEifQ...",
"token_type": "Bearer",
"expires_in": 3600
}
If the authorization request included the offline_access scope, the response also includes a refresh_token:
{
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InlvcmF1dGgta2V5LTEifQ...",
"refresh_token": "oidcrt_64characterrandomstring",
"token_type": "Bearer",
"expires_in": 3600
}
| Field | Description |
|---|---|
id_token | RS256-signed JWT containing user identity claims |
refresh_token | Opaque token for obtaining new ID tokens. Prefixed with oidcrt_. Only present when offline_access was requested. |
token_type | Always Bearer |
expires_in | ID token validity in seconds (default: 3600) |
Refresh Token Grant
When the ID token expires, use the refresh_token grant to obtain a new ID token without requiring the user to log in again.
YorAuth implements refresh token rotation — each use of a refresh token revokes it and issues a new one.
Request
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be refresh_token |
refresh_token | Yes | The refresh token received from a previous token response |
client_id | Yes | Your client's client_id |
client_secret | Yes | Your client's secret |
curl -X POST https://api.yorauth.com/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=oidcrt_your-refresh-token" \
-d "client_id=oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345" \
-d "client_secret=your-client-secret"
const response = await fetch('https://api.yorauth.com/oidc/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: 'oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345',
client_secret: 'your-client-secret',
}),
});
const tokens = await response.json();
// Replace stored refresh_token with tokens.refresh_token
Successful Response
{
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InlvcmF1dGgta2V5LTEifQ...",
"refresh_token": "oidcrt_newrandomstring",
"token_type": "Bearer",
"expires_in": 3600
}
The original refresh token is revoked immediately. The new refresh_token in the response must replace the old one in your storage. Scopes are preserved across rotation — the new ID token contains the same scopes as the original.
Refresh Token Reuse Detection
If a refresh token that has already been revoked is presented, YorAuth detects a potential token theft and revokes the entire token family (all refresh tokens for that user and client). This forces the user to re-authenticate.
{
"error": "invalid_grant",
"error_description": "Refresh token has been revoked."
}
ID Token Claims
The id_token is a standard JWT. After decoding (and verifying the RS256 signature), the payload looks like:
{
"iss": "https://api.yorauth.com",
"sub": "01932c4d-f1a2-7000-b3c4-d5e6f7a8b9c0",
"aud": "oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345",
"iat": 1740480000,
"exp": 1740483600,
"scope": "openid profile email",
"nonce": "abc123def456",
"email": "alice@example.com",
"email_verified": true,
"name": "Alice Example",
"picture": "https://yourapp.com/avatars/alice.png"
}
Claims present in the token depend on the scopes requested:
| Claim | Scope Required | Always Present |
|---|---|---|
iss | — | Yes |
sub | — | Yes |
aud | — | Yes |
iat | — | Yes |
exp | — | Yes |
scope | — | Yes |
nonce | — | Only if provided in auth request |
email | email | No |
email_verified | email | No |
name | profile | No |
picture | profile | No |
Verifying the ID Token
import * as jose from 'jose';
const JWKS = jose.createRemoteJWKSet(
new URL('https://api.yorauth.com/.well-known/jwks.json')
);
const { payload } = await jose.jwtVerify(idToken, JWKS, {
issuer: 'https://api.yorauth.com',
audience: 'oidc_AbCdEfGhIjKlMnOpQrStUvWxYz012345',
});
console.log(payload.sub); // user UUID
console.log(payload.email); // user's email (if email scope was granted)
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
$jwks = json_decode(file_get_contents('https://api.yorauth.com/.well-known/jwks.json'), true);
$keys = JWK::parseKeySet($jwks);
$decoded = JWT::decode($idToken, $keys);
echo $decoded->sub; // user UUID
echo $decoded->email; // user's email
UserInfo Endpoint
To retrieve claims in JSON form rather than from the ID token:
GET /oidc/userinfo
Authorization: Bearer <id_token>
The response contains the same claims as the ID token, scoped to what was granted. This endpoint requires a valid JWT Bearer token and is rate-limited.
{
"sub": "01932c4d-f1a2-7000-b3c4-d5e6f7a8b9c0",
"email": "alice@example.com",
"email_verified": true,
"name": "Alice Example",
"picture": null
}
Error Responses
All errors follow the OAuth 2.0 error format:
{
"error": "invalid_grant",
"error_description": "Authorization code has expired."
}
| Error | HTTP | Cause |
|---|---|---|
unsupported_grant_type | 400 | grant_type is not authorization_code or refresh_token |
invalid_grant | 400 | Invalid code, expired code, code already used, PKCE failure, redirect URI mismatch, invalid or revoked refresh token |
invalid_client | 400 | client_id not found, inactive, or client_secret incorrect |
| Validation errors | 422 | Required parameters missing |
Common Error Messages
error_description | Meaning |
|---|---|
Authorization code has expired. | More than 10 minutes elapsed since authorization |
Authorization code has already been used. | Code replay — all related tokens have been revoked |
PKCE verification failed. | code_verifier does not match code_challenge |
PKCE code_verifier is required. | code_verifier parameter missing |
Redirect URI mismatch. | redirect_uri does not match value from authorization request |
Invalid client credentials. | client_secret incorrect |
Refresh token has been revoked. | Revoked token presented — token family has been revoked |
Refresh token has expired. | Refresh token lifetime exceeded |