338 lines
9.7 KiB
Markdown
338 lines
9.7 KiB
Markdown
# JWT Refresh Token Implementation Guide
|
|
|
|
## Overview
|
|
|
|
The JWT authentication system supports both **cookie-based** and **header-based** (Bearer token) authentication with comprehensive refresh token functionality and proper logout logic. **All authentication methods now use refresh tokens** - there is no legacy single-token mode.
|
|
|
|
## Features
|
|
|
|
- **Dual Authentication Methods**: Support for both cookie-based and Bearer token authentication
|
|
- **Universal Refresh Tokens**: All logins receive both access and refresh tokens
|
|
- **Automatic Token Refresh**: Tokens are refreshed when 75% of their lifetime has passed
|
|
- **Logout Functionality**: Proper token blacklisting and cleanup
|
|
- **Security**: Short-lived access tokens (30 minutes) and longer-lived refresh tokens (7 days)
|
|
|
|
## Authentication Methods
|
|
|
|
### 1. Cookie-Based Authentication
|
|
- Access token stored in `auth_token` cookie
|
|
- Refresh token stored in `refresh_token` cookie
|
|
- Suitable for web applications with same-origin requests
|
|
- Tokens also returned in response body
|
|
|
|
### 2. Bearer Token Authentication
|
|
- Access token sent in `Authorization: Bearer <token>` header
|
|
- Refresh token sent in `X-Refresh-Token` header
|
|
- Suitable for mobile apps, SPAs, and API integrations
|
|
- Tokens returned in response body
|
|
|
|
## API Endpoints
|
|
|
|
### Login
|
|
```http
|
|
POST /api/user/login
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"username": "user@example.com",
|
|
"password": "password123"
|
|
}
|
|
```
|
|
|
|
**Response (all logins):**
|
|
```json
|
|
{
|
|
"user": { ... },
|
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
}
|
|
```
|
|
|
|
For cookie-based auth, tokens are also set as httpOnly cookies.
|
|
|
|
### Refresh Token
|
|
```http
|
|
POST /api/user/refresh-token
|
|
```
|
|
|
|
**For Cookie-based auth:**
|
|
- Refresh token is read from `refresh_token` cookie
|
|
- New tokens are set as cookies AND returned in response body
|
|
|
|
**For Bearer token auth:**
|
|
```http
|
|
POST /api/user/refresh-token
|
|
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Tokens refreshed successfully",
|
|
"accessToken": "new_access_token",
|
|
"refreshToken": "new_refresh_token"
|
|
}
|
|
```
|
|
|
|
### Logout
|
|
```http
|
|
POST /api/user/logout
|
|
Authorization: Bearer <access_token>
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"success": true
|
|
}
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
```env
|
|
# JWT Configuration
|
|
JWT_SECRET=your-secret-key-for-access-tokens
|
|
JWT_REFRESH_SECRET=your-secret-key-for-refresh-tokens
|
|
|
|
# Access Token Expiry (use one of these)
|
|
JWT_ACCESS_TOKEN_EXPIRY=1800 # Access token expiry in seconds (30 minutes)
|
|
JWT_ACCESS_TOKEN_EXPIRATION=30m # Access token expiry (supports s, m, h, d)
|
|
JWT_EXPIRY=1800 # Legacy: Access token expiry in seconds
|
|
JWT_EXPIRATION=30m # Legacy: Access token expiry with duration
|
|
|
|
# Refresh Token Expiry (use one of these)
|
|
JWT_REFRESH_TOKEN_EXPIRY=604800 # Refresh token expiry in seconds (7 days)
|
|
JWT_REFRESH_TOKEN_EXPIRATION=7d # Refresh token expiry (supports s, m, h, d)
|
|
JWT_REFRESH_EXPIRATION=7d # Legacy: Refresh token expiry with duration
|
|
|
|
# Cookie Names (optional)
|
|
JWT_COOKIE_NAME=auth_token # Access token cookie name (default: auth_token)
|
|
JWT_REFRESH_COOKIE_NAME=refresh_token # Refresh token cookie name (default: refresh_token)
|
|
```
|
|
|
|
### Environment Variable Priority
|
|
|
|
**Access Token Expiry** (checked in order):
|
|
1. `JWT_ACCESS_TOKEN_EXPIRY` (seconds)
|
|
2. `JWT_ACCESS_TOKEN_EXPIRATION` (duration string)
|
|
3. `JWT_EXPIRY` (seconds) - legacy
|
|
4. `JWT_EXPIRATION` (duration string) - legacy
|
|
5. Default: 1800 seconds (30 minutes)
|
|
|
|
**Refresh Token Expiry** (checked in order):
|
|
1. `JWT_REFRESH_TOKEN_EXPIRY` (seconds)
|
|
2. `JWT_REFRESH_TOKEN_EXPIRATION` (duration string)
|
|
3. `JWT_REFRESH_EXPIRATION` (duration string) - legacy
|
|
4. Default: 604800 seconds (7 days)
|
|
|
|
### Duration String Format
|
|
Supports: `s` (seconds), `m` (minutes), `h` (hours), `d` (days)
|
|
Examples: `30s`, `15m`, `2h`, `7d`
|
|
|
|
## Token Structure
|
|
|
|
### Access Token Payload
|
|
```json
|
|
{
|
|
"userId": "user-uuid",
|
|
"authLevel": 0,
|
|
"userStatus": 1,
|
|
"orgId": "org-uuid",
|
|
"type": "access",
|
|
"iat": 1640995200,
|
|
"exp": 1640997000
|
|
}
|
|
```
|
|
|
|
### Refresh Token Payload
|
|
```json
|
|
{
|
|
"userId": "user-uuid",
|
|
"orgId": "org-uuid",
|
|
"type": "refresh",
|
|
"iat": 1640995200,
|
|
"exp": 1641600000
|
|
}
|
|
```
|
|
|
|
## Automatic Token Refresh
|
|
|
|
The system automatically refreshes tokens when:
|
|
- Token is within 25% of its expiration time (75% of lifetime has passed)
|
|
- Valid refresh token is available
|
|
- User makes an authenticated request
|
|
|
|
**✅ Automatic refresh happens on every authenticated API call** - no manual intervention needed!
|
|
|
|
### Response Headers
|
|
For Bearer token authentication, refresh responses include:
|
|
- `X-New-Access-Token`: New access token
|
|
- `X-New-Refresh-Token`: New refresh token
|
|
- `X-Token-Refreshed`: "true" indicator
|
|
|
|
### Manual Refresh (Optional)
|
|
|
|
While automatic refresh handles most scenarios, manual refresh is available for:
|
|
- **Proactive refresh**: Before critical operations
|
|
- **Background apps**: Long-running applications that need fresh tokens
|
|
- **Offline recovery**: When app reconnects after being offline
|
|
|
|
```http
|
|
POST /api/user/refresh-token
|
|
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
```
|
|
|
|
## Client Implementation Examples
|
|
|
|
### JavaScript/TypeScript (Fetch API)
|
|
|
|
```typescript
|
|
class ApiClient {
|
|
private accessToken: string = '';
|
|
private refreshToken: string = '';
|
|
|
|
async login(username: string, password: string) {
|
|
const response = await fetch('/api/user/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
this.accessToken = data.token;
|
|
this.refreshToken = data.refreshToken; // Always present now
|
|
return data;
|
|
}
|
|
|
|
async makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
|
|
const headers = {
|
|
'Authorization': `Bearer ${this.accessToken}`,
|
|
...options.headers
|
|
};
|
|
|
|
let response = await fetch(url, { ...options, headers });
|
|
|
|
// Automatically handle token refresh (tokens updated in response headers)
|
|
if (response.headers.get('X-Token-Refreshed') === 'true') {
|
|
const newAccessToken = response.headers.get('X-New-Access-Token');
|
|
const newRefreshToken = response.headers.get('X-New-Refresh-Token');
|
|
|
|
if (newAccessToken) this.accessToken = newAccessToken;
|
|
if (newRefreshToken) this.refreshToken = newRefreshToken;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// Optional: Manual refresh (usually not needed due to automatic refresh)
|
|
async refreshTokens() {
|
|
const response = await fetch('/api/user/refresh-token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Refresh-Token': this.refreshToken
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.accessToken = data.accessToken;
|
|
this.refreshToken = data.refreshToken;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async logout() {
|
|
await fetch('/api/user/logout', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${this.accessToken}` }
|
|
});
|
|
|
|
this.accessToken = '';
|
|
this.refreshToken = '';
|
|
}
|
|
}
|
|
```
|
|
|
|
### React Hook Example
|
|
|
|
```typescript
|
|
import { useState, useCallback } from 'react';
|
|
|
|
export const useAuth = () => {
|
|
const [accessToken, setAccessToken] = useState<string>('');
|
|
const [refreshToken, setRefreshToken] = useState<string>('');
|
|
|
|
const login = useCallback(async (username: string, password: string) => {
|
|
const response = await fetch('/api/user/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
setAccessToken(data.token);
|
|
setRefreshToken(data.refreshToken); // Always present
|
|
return data;
|
|
}, []);
|
|
|
|
const logout = useCallback(async () => {
|
|
if (accessToken) {
|
|
await fetch('/api/user/logout', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${accessToken}` }
|
|
});
|
|
}
|
|
setAccessToken('');
|
|
setRefreshToken('');
|
|
}, [accessToken]);
|
|
|
|
return { accessToken, refreshToken, login, logout };
|
|
};
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
1. **Token Blacklisting**: Logout tokens are blacklisted in Redis with TTL matching token expiration
|
|
2. **Short-lived Access Tokens**: 30-minute expiry reduces exposure window
|
|
3. **Secure Cookies**: httpOnly, secure, sameSite attributes for cookie-based auth
|
|
4. **Token Rotation**: Refresh tokens are rotated on each refresh
|
|
5. **Environment-specific Secrets**: Different secrets for access and refresh tokens
|
|
|
|
## Migration Guide
|
|
|
|
### From Single Token to Refresh Token System
|
|
|
|
Since this is a new implementation, all clients should expect:
|
|
|
|
1. **Login Response**: Always includes both `token` (access) and `refreshToken`
|
|
2. **Token Storage**: Store both tokens securely
|
|
3. **API Requests**: Use access token in Authorization header
|
|
4. **Automatic Refresh**: Tokens refresh automatically - just watch for response headers
|
|
5. **Logout**: Call logout endpoint to invalidate tokens
|
|
|
|
**Key Point**: Manual refresh is optional since automatic refresh handles token renewal seamlessly.
|
|
|
|
**No backward compatibility needed** - this is the only authentication method.
|
|
|
|
### Testing
|
|
|
|
```bash
|
|
# Login and get tokens
|
|
curl -X POST http://localhost:3000/api/user/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username": "test@example.com", "password": "password"}'
|
|
|
|
# Use access token
|
|
curl -X GET http://localhost:3000/api/user/profile \
|
|
-H "Authorization: Bearer <access_token>"
|
|
|
|
# Refresh tokens
|
|
curl -X POST http://localhost:3000/api/user/refresh-token \
|
|
-H "X-Refresh-Token: <refresh_token>"
|
|
|
|
# Logout
|
|
curl -X POST http://localhost:3000/api/user/logout \
|
|
-H "Authorization: Bearer <access_token>"
|
|
``` |