Token Endpoints
Technical reference for the OAuth 2.0 token, refresh, and revocation endpoints.
Token lifecycle
Token endpoint
POST /api/oauth/tokenExchanges an authorization code for an access token, or refreshes an existing token.
Authorization Code grant
| Parameter | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be authorization_code |
code | string | Yes | The authorization code from the callback |
redirect_uri | string | Yes | Must match the URI used in the authorization request |
client_id | string | Yes | Your application's client ID |
code_verifier | string | Yes | The PKCE code verifier (plain-text, 43-128 chars) |
client_secret | string | Conditional | Required for confidential clients |
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "def456...",
"scope": "openid courses:read students:read",
"created_at": 1710000000,
"school_id": "550e8400-e29b-41d4-a716-446655440000",
"school_subdomain": "demo"
}| Field | Description |
|---|---|
access_token | Bearer token for API requests |
token_type | Always Bearer |
expires_in | Token lifetime in seconds (default: 7200 / 2 hours) |
refresh_token | Token to obtain a new access token |
scope | Granted scopes (may be a subset of what was requested) |
school_id | The UUID of the authorized school |
school_subdomain | The subdomain of the authorized school |
Refresh Token grant
| Parameter | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be refresh_token |
refresh_token | string | Yes | A valid refresh token |
client_id | string | Yes | Your application's client ID |
client_secret | string | Conditional | Required for confidential clients |
Response
Same format as the authorization code grant response.
Refresh token rotation
Every successful refresh returns a new refresh_token in the response. The previous refresh token is immediately revoked.
Your client must persist the new refresh token and use it for the next refresh. Replaying an old token returns invalid_grant.
async function refreshAccessToken(storedRefreshToken) {
const res = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: storedRefreshToken,
client_id: CLIENT_ID,
// Include client_secret for confidential clients:
// client_secret: CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Refresh failed: ${res.status}`);
const { access_token, refresh_token, expires_in } = await res.json();
// Always persist the latest refresh_token
await store.set("oauth_tokens", {
access_token,
refresh_token: refresh_token ?? storedRefreshToken,
expires_at: Date.now() + expires_in * 1000,
});
return access_token;
}Revocation endpoint
POST /api/oauth/revokeRevokes an access token or refresh token per RFC 7009.
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The token to revoke |
token_type_hint | string | No | access_token or refresh_token |
client_id | string | Yes | Your application's client ID |
The endpoint always returns 200 OK, even if the token was already revoked or invalid. This prevents token-guessing attacks.
Introspection endpoint
POST /api/oauth/introspectReturns metadata about a token per RFC 7662.
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The token to introspect |
client_id | string | Yes | Your application's client ID |
Response (active token)
{
"active": true,
"scope": "openid courses:read",
"client_id": "abc123",
"token_type": "Bearer",
"exp": 1710007200
}Response (inactive/invalid token)
{
"active": false
}UserInfo endpoint
GET /api/oauth/userinfo
POST /api/oauth/userinfoReturns identity claims about the authenticated user per OpenID Connect Core 1.0 §5.3. The openid scope is required.
Request
Include the access token as a Bearer token in the Authorization header:
Authorization: Bearer <access_token>Response
The claims returned depend on the scopes granted to the token:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true
}| Claim | Scope required | Description |
|---|---|---|
sub | openid | Unique user identifier (UUID) |
name | profile | User's display name |
email | email | User's email address |
email_verified | email | Whether the email has been verified |
Error responses
| HTTP Status | Error | When |
|---|---|---|
| 401 | invalid_token | Missing, expired, or revoked token |
| 403 | invalid_scope | Token does not include the openid scope |
Error responses
All token endpoints return errors in the standard OAuth 2.0 format:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired or has already been used."
}| Error | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Missing or malformed parameter |
invalid_client | 401 | Client authentication failed |
invalid_grant | 400 | Code expired, already used, or verifier mismatch |
unauthorized_client | 400 | Client not authorized for this grant type |
unsupported_grant_type | 400 | Only authorization_code and refresh_token are supported |