Appearance
Refresh Token Flow
This page documents the token refresh flow in GrydAuth, which allows users to obtain new access tokens without re-entering their credentials.
v5 (Epic 533) — refresh token in an HttpOnly cookie. The refresh token is no longer returned in the JSON body. It is delivered exclusively in a
Set-Cookie: __Secure-gryd.refresh; HttpOnly; Secure; SameSite; Path=/api/v1/authcookie (inaccessible to JavaScript).POST /api/v1/auth/refreshreads the token from that cookie — the request body carries only an optionaltenantId. Because the endpoint is now cookie-authenticated it requires the CSRF headerX-Gryd-Csrfmatching the readablegryd.csrfcookie (double-submit), and the SPA must sendcredentials: include. A missing refresh cookie →401; a missing/mismatched CSRF header →403. The single-use rotation rewrites the cookie on every refresh, and/logoutexpires it. Seetoken-storage-backend-change-inventory.md.
Overview
Access tokens in GrydAuth v5 are short-lived by design — capped at 15 minutes, default 10 — because their roles/permissions are a snapshot at issuance time. When an access token expires, the client can use the refresh token to obtain a new access token without requiring the user to log in again.
Token Lifetimes
| Token Type | Default Lifetime | Configurable? | Purpose |
|---|---|---|---|
| Access Token | 10 minutes | Yes — JwtSettings:ExpirationMinutes, validated at boot, must be 1–15 | API authentication |
| Refresh Token | 7 days | No — hardcoded default parameter in StatelessRefreshTokenService, not read from JwtSettings | Obtain new access tokens |
| Global Token | 2 minutes | No — hardcoded in JwtTokenService | Tenant selection / first login only, see Switch Tenant |
Refresh-token TTL is not currently configurable
Unlike the access-token TTL, the 7-day refresh-token lifetime is a hardcoded default parameter, not a JwtSettings/GrydAuth config key. Don't document or rely on a config toggle for it — if you need it configurable, that's a gap to raise with the Auth team, not an existing setting.
Sequence Diagram
100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar
Step-by-Step Explanation
Phase 1: Access Token Expiration
When the access token expires, API requests return 401 Unauthorized with a ProblemDetails body carrying code: "TOKEN_EXPIRED". The client should detect this and initiate the refresh flow. See the canonical error code matrix.
Phase 2: Token Refresh
2.1 Send Refresh Request
http
POST /api/v1/auth/refresh
Cookie: __Secure-gryd.refresh=<httponly>; gryd.csrf=<value>
X-Gryd-Csrf: <value>
Content-Type: application/json
{
"tenantId": "550e8400-e29b-41d4-a716-446655440000"
}tenantId is optional — TenantValidationBehavior injects it automatically from context, but a client may override it.
The response is the same AuthenticationResult shape as Login — the token field is token, not accessToken, and current roles/permissions/tenant info are included, not just a bare token pair.
2.2 Validate Refresh Token
The refresh token is validated against Redis cache:
csharp
var tokenData = await _cache.GetAsync<RefreshTokenData>(refreshToken);
if (tokenData == null)
return Result.Failure("Invalid refresh token");2.3 Validate User Status
The system ensures:
- User still exists
- User is active (
IsActive = true) - User is not locked out
2.4 Token Version Check
Token version prevents use of old tokens after:
- Password change
- Admin-forced logout
- Security incident response
csharp
if (tokenData.TokenVersion != user.TokenVersion)
{
await _cache.DeleteAsync(refreshToken);
return Result.Failure("Token invalidated");
}2.5 Tenant Validation
Ensures the user still has access to the tenant from the original token.
2.6 Refresh Roles & Permissions
Important: The new access token contains the current roles and permissions, not the cached ones. This ensures permission changes take effect immediately.
2.7 Token Rotation
For security, GrydAuth implements refresh token rotation:
- Old refresh token is deleted
- New refresh token is generated
- Client must use new refresh token for next refresh
This limits the damage if a refresh token is compromised.
Phase 3: Resume Operations
Client stores the new tokens and continues making API requests.
Automatic Token Refresh
Client-Side Implementation
typescript
// Axios interceptor for automatic refresh
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 &&
error.response?.data?.code === 'TOKEN_EXPIRED' &&
!originalRequest._retry) {
originalRequest._retry = true;
try {
const { token, refreshToken } = await authService.refresh();
authStore.setTokens(token, refreshToken);
originalRequest.headers.Authorization = `Bearer ${token}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh failed - redirect to login
authStore.clearTokens();
router.push('/login');
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);Token Refresh Service
typescript
class AuthService {
private refreshPromise: Promise<AuthenticationResult> | null = null;
async refresh(): Promise<AuthenticationResult> {
// Prevent multiple simultaneous refresh requests
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.doRefresh();
try {
return await this.refreshPromise;
} finally {
this.refreshPromise = null;
}
}
private async doRefresh(): Promise<AuthenticationResult> {
const refreshToken = authStore.getRefreshToken();
const response = await api.post('/auth/refresh', { refreshToken });
return response.data;
}
}Sliding Expiration (Rotation Side-Effect, Not a Toggle)
There is no EnableSlidingExpiration setting. The sliding behavior below falls out naturally from rotation: every successful refresh issues a brand-new refresh token with a fresh 7-day expiry (the old one is deleted immediately). As long as the user keeps refreshing within 7 days, the session effectively never expires:
100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar
Error Scenarios
| Error | HTTP Status | Cause | Client Action |
|---|---|---|---|
| Invalid refresh token | 401 | Token not found in cache | Redirect to login |
| Token expired | 401 | Refresh token TTL exceeded | Redirect to login |
| Token revoked | 401 | Password changed or admin action | Redirect to login |
| User inactive | 401 | Account deactivated | Show account disabled message |
| Tenant access revoked | 403 | Removed from tenant | Redirect to tenant selection |
| Reuse detection unavailable | 401 | Cache/Redis outage — rotation denied (fail-secure, v5) | Redirect to login |
| Rate limit exceeded | 429 | Too many refresh attempts | Back off, honor Retry-After |
Security Considerations
Refresh Token Storage
| Platform | Recommended Storage |
|---|---|
| Web (SPA) | HttpOnly cookie or memory |
| Mobile | Secure storage (Keychain/Keystore) |
| Desktop | OS credential manager |
Never Store in localStorage
Refresh tokens should never be stored in localStorage due to XSS vulnerability.
Token Rotation Benefits
- Limits exposure window - Stolen tokens become invalid after one use
- Detects theft - If legitimate user's refresh fails, theft is detected
- Audit trail - Each rotation is logged for security analysis
Degraded Mode (Fail-Secure)
Single-use rotation depends on the reuse-detection store (Redis). If that store is unavailable, GrydAuth v5 denies rotation by default and forces re-authentication, rather than issuing a long-lived refresh token without anti-reuse protection (as v4 did under load). The policy is pluggable via IDegradedModeRefreshPolicy (default DenyAllDegradedModeRefreshPolicy); any alternative that issues a token in degraded mode must require MFA step-up, use a very short TTL, and stay behind a feature flag that is off by default. Denials are logged as security events. See the v4 → v5 migration guide.
Absolute Expiration
Recommended, not currently implemented
There is no enforced absolute maximum lifetime today — a user who refreshes regularly can stay signed in indefinitely on the rolling 7-day window above. An AbsoluteExpiration cap is a sensible hardening candidate, but don't document it as existing behavior; verify against StatelessRefreshTokenService before relying on it.
Configuration
jsonc
{
"JwtSettings": {
"ExpirationMinutes": 10 // access-token TTL; validated at boot, must be 1-15
}
}Refresh-token TTL (7 days) and rotation are not configuration-driven — they're hardcoded in StatelessRefreshTokenService. Access tokens are deliberately short-lived (maximum 15 minutes, default 10) because their authorization claims are snapshots. Refresh-token rotation preserves session continuity, while the shorter access-token TTL bounds exposure to stale claims.
Related Flows
- Login Flow - Initial authentication
- Logout Flow - Token invalidation
- Switch Tenant - Changing tenant context