Token Management
YorAuth uses a hybrid token model: short-lived JWT access tokens for stateless API authentication combined with long-lived opaque refresh tokens for session persistence. This gives you the performance of stateless validation with the security of revocable sessions.
Token Types
Access Token (JWT)
The access token is a signed JSON Web Token (JWT) that your application uses to authenticate API requests. It is validated by verifying the RS256 signature against YorAuth's public key — no database lookup required.
Properties:
- Format: RS256-signed JWT
- Lifetime: 15 minutes (
expires_in: 900) - Storage: Client-side memory (do not store in
localStoragein browser environments) - Revocation: Not directly revocable (short lifetime limits exposure)
Refresh Token (Opaque)
The refresh token is a cryptographically random opaque string, prefixed with ref_. It is stored as a SHA-256 hash in the database. Use it to obtain a new access token when the current one expires.
Properties:
- Format:
ref_followed by 64 random characters - Lifetime: 7 days standard, 30 days with
remember_me: true - Storage: Secure client-side storage (httpOnly cookie recommended for web apps)
- Revocation: Immediate via database flag
- Rotation: A new refresh token is issued on every use
JWT Payload
The access token payload contains the following claims:
{
"jti": "550e8400-e29b-41d4-a716-446655440000",
"sub": "user-uuid-here",
"app": "application-uuid-here",
"email": "jane@example.com",
"iat": 1708875300,
"exp": 1708876200
}
| Claim | Description |
|---|---|
jti | Unique token ID (UUID). Used for blacklisting on logout. |
sub | The authenticated user's ID (UUID). |
app | The application ID this token belongs to. |
email | The user's email address. |
iat | Issued at (Unix timestamp). |
exp | Expiration time (Unix timestamp). iat + 900 seconds. |
If your application has custom JWT claims configured, additional claims are merged into the payload. Standard claims always take precedence over custom claims.
Impersonation claim: When an admin impersonates a user, the token includes an act claim per RFC 8693:
{
"sub": "impersonated-user-uuid",
"act": {
"sub": "admin-user-uuid"
}
}
Using the Access Token
Include the access token in the Authorization header on every request to a protected endpoint:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
curl https://api.yorauth.com/api/v1/applications/your-application-id/users/550e8400-e29b-41d4-a716-446655440000/profile \
-H "Authorization: Bearer <access_token>"
const response = await fetch(
`https://api.yorauth.com/api/v1/applications/your-application-id/users/${userId}/profile`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
When the access token expires, the API returns 401 with error code AUTH_TOKEN_EXPIRED. At that point, use the refresh token to obtain a new pair.
Refresh Token Rotation
Obtain a new access token (and a new refresh token) by presenting the current refresh token. The old refresh token is immediately revoked.
Endpoint: POST /api/v1/applications/{applicationId}/users/token/refresh
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token | string | Yes | The current refresh token. |
curl -X POST https://api.yorauth.com/api/v1/applications/your-application-id/users/token/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "ref_a1b2c3d4e5f6..."}'
const response = await fetch(
'https://api.yorauth.com/api/v1/applications/your-application-id/users/token/refresh',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
}
);
const data = await response.json();
// Store the new tokens — the old refresh token is now invalid
storeAccessToken(data.data.access_token);
storeRefreshToken(data.data.refresh_token);
Response 200 OK
{
"data": {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "ref_newtoken1234567890...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jane@example.com",
"name": "Jane Doe",
"email_verified": true,
"created_at": "2026-02-25T10:00:00Z",
"metadata": null
}
}
}
Always replace both the access token and the refresh token after a refresh. The old refresh token is revoked and cannot be used again. Storing stale refresh tokens causes AUTH_INVALID_REFRESH_TOKEN errors.
The is_remember_me flag from the original login is carried forward through token rotation. A remember-me session remains a 30-day session through all rotations.
Error Responses
| HTTP Status | Error Code | Description |
|---|---|---|
| 401 | AUTH_INVALID_REFRESH_TOKEN | Token is invalid, expired, already revoked, or a reuse was detected. |
Token Rotation and Reuse Detection
Every time a refresh token is used, it is rotated:
- The old token is marked as revoked with reason
rotated. - A new refresh token is generated and linked to the old one via
parent_token_id. - The new token is returned to the client.
If a revoked refresh token is used, this indicates potential token theft. YorAuth responds by immediately revoking all active sessions for the affected user. The error returned is still AUTH_INVALID_REFRESH_TOKEN, but the user will find all their sessions terminated and must re-authenticate.
This automatic family revocation limits the damage window even if a refresh token was exfiltrated.
Remember Me
When logging in with remember_me: true, the refresh token lifetime is extended from 7 days to 30 days:
{
"email": "jane@example.com",
"password": "Str0ng!Passw0rd",
"remember_me": true
}
The is_remember_me flag is stored on the session record and is carried forward through all subsequent token rotations. You can identify remember-me sessions by the is_remember_me: true field in the sessions list.
Token Lifetime Reference
| Token | Standard | With remember_me |
|---|---|---|
| Access token | 15 minutes | 15 minutes (unchanged) |
| Refresh token | 7 days | 30 days |
Implementing Auto-Refresh
A well-behaved client proactively refreshes the access token before it expires, or retries with a refresh on receiving a 401. The following pattern handles concurrent requests safely:
class TokenManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.refreshPromise = null;
this.applicationId = 'your-application-id';
}
async request(url, options = {}) {
const response = await this.makeRequest(url, options);
if (response.status === 401) {
// Prevent concurrent refresh calls
await this.refresh();
return this.makeRequest(url, options);
}
return response;
}
async makeRequest(url, options = {}) {
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
});
}
async refresh() {
// De-duplicate concurrent refresh calls
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = (async () => {
const response = await fetch(
`https://api.yorauth.com/api/v1/applications/${this.applicationId}/users/token/refresh`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: this.refreshToken })
}
);
if (!response.ok) {
// Session expired — redirect to login
this.accessToken = null;
this.refreshToken = null;
throw new Error('Session expired');
}
const data = await response.json();
this.accessToken = data.data.access_token;
this.refreshToken = data.data.refresh_token;
})();
try {
await this.refreshPromise;
} finally {
this.refreshPromise = null;
}
}
}
Public Key for Verification
YorAuth publishes its current RSA public keys at the JWKS endpoint. Use this to verify access token signatures in your own services without calling YorAuth on every request:
GET /.well-known/jwks.json
Response
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "xjlCRBqkgU...",
"e": "AQAB"
}
]
}
Standard JWT libraries can consume this endpoint directly for key discovery and automatic rotation support.