Skip to main content

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.

text
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

text
POST /oidc/token
Content-Type: application/x-www-form-urlencoded
ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesThe authorization code from the redirect
redirect_uriYesMust exactly match the redirect_uri used in the authorization request
client_idYesYour client's client_id
client_secretYesYour client's secret
code_verifierYesThe PKCE code_verifier generated before the authorization request
bash
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"
javascript
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();
php
$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

json
{
  "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:

json
{
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InlvcmF1dGgta2V5LTEifQ...",
  "refresh_token": "oidcrt_64characterrandomstring",
  "token_type": "Bearer",
  "expires_in": 3600
}
FieldDescription
id_tokenRS256-signed JWT containing user identity claims
refresh_tokenOpaque token for obtaining new ID tokens. Prefixed with oidcrt_. Only present when offline_access was requested.
token_typeAlways Bearer
expires_inID 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

ParameterRequiredDescription
grant_typeYesMust be refresh_token
refresh_tokenYesThe refresh token received from a previous token response
client_idYesYour client's client_id
client_secretYesYour client's secret
bash
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"
javascript
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

json
{
  "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.

json
{
  "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:

json
{
  "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:

ClaimScope RequiredAlways Present
issYes
subYes
audYes
iatYes
expYes
scopeYes
nonceOnly if provided in auth request
emailemailNo
email_verifiedemailNo
nameprofileNo
pictureprofileNo

Verifying the ID Token

javascript
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)
php
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:

text
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.

json
{
  "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:

json
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired."
}
ErrorHTTPCause
unsupported_grant_type400grant_type is not authorization_code or refresh_token
invalid_grant400Invalid code, expired code, code already used, PKCE failure, redirect URI mismatch, invalid or revoked refresh token
invalid_client400client_id not found, inactive, or client_secret incorrect
Validation errors422Required parameters missing

Common Error Messages

error_descriptionMeaning
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