11 KiB
Refresh Token Implementation Guide
Overview
This document describes the complete refresh token system implemented for the NestJS Retail POS backend. The implementation includes token rotation, secure storage, and automatic cleanup for enhanced security.
Features Implemented
1. Refresh Token Storage
- Database Table:
refresh_tokens - Hashed Storage: Tokens are hashed using SHA-256 before storage
- Expiration: 7 days (configurable via environment variable)
- Revocation Support: Tokens can be individually or bulk revoked
- Cascade Delete: Tokens automatically deleted when user is deleted
2. Token Rotation
The system implements refresh token rotation for enhanced security:
- When a refresh token is used, it is immediately revoked
- A new refresh token is issued along with the new access token
- This prevents token reuse and mitigates replay attacks
3. Security Features
- Hashed Storage: Tokens are stored as SHA-256 hashes
- Unique Tokens: Each token is cryptographically unique (64 bytes random)
- Indexed Queries: Fast lookups with database indexes
- Expiration Checks: Automatic expiration validation
- User Validation: Active user checks on every refresh
- Revocation: Individual and bulk token revocation
4. Automatic Cleanup
Optional background task to clean up old tokens:
- Removes expired tokens
- Removes revoked tokens
- Removes tokens older than 30 days
- Runs daily at 2:00 AM (configurable)
Database Schema
refresh_tokens Table
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
token VARCHAR(500) UNIQUE NOT NULL, -- Hashed token
userId UUID NOT NULL,
expiresAt TIMESTAMP NOT NULL,
isRevoked BOOLEAN DEFAULT FALSE,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_refresh_tokens_token (token),
INDEX idx_refresh_tokens_user_id (userId)
);
API Endpoints
1. Login (POST /api/auth/login)
Returns both access token and refresh token.
Request:
{
"email": "user@example.com",
"password": "Password123!"
}
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6...",
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "John Doe",
"roles": ["user"],
"isActive": true,
"createdAt": "2025-01-15T10:00:00.000Z"
}
}
2. Refresh Token (POST /api/auth/refresh)
Exchange refresh token for new access token and refresh token.
Request:
{
"refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6..."
}
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6...",
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "John Doe",
"roles": ["user"],
"isActive": true,
"createdAt": "2025-01-15T10:00:00.000Z"
}
}
Behavior:
- Old refresh token is automatically revoked (token rotation)
- New refresh token is issued
- New access token is issued
3. Logout (POST /api/auth/logout)
Revokes the refresh token.
Request:
{
"refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6..."
}
Response:
{
"success": true,
"message": "Logged out successfully"
}
4. Revoke All Tokens (POST /api/auth/revoke-all)
Revokes all refresh tokens for the authenticated user (requires JWT).
Headers:
Authorization: Bearer <access_token>
Response:
{
"success": true,
"message": "All refresh tokens revoked successfully"
}
Use Cases:
- Logout from all devices
- Security breach response
- Account security settings
Client Implementation Guide
1. Login Flow
// Login
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { access_token, refresh_token, user } = await loginResponse.json();
// Store tokens securely
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
2. API Request with Token Refresh
async function apiRequest(url: string, options: RequestInit = {}) {
// Add access token to request
const accessToken = localStorage.getItem('access_token');
options.headers = {
...options.headers,
'Authorization': `Bearer ${accessToken}`
};
let response = await fetch(url, options);
// If 401, try to refresh token
if (response.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (refreshResponse.ok) {
const { access_token, refresh_token: newRefreshToken } =
await refreshResponse.json();
// Update stored tokens
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', newRefreshToken);
// Retry original request
options.headers['Authorization'] = `Bearer ${access_token}`;
response = await fetch(url, options);
} else {
// Refresh failed, redirect to login
window.location.href = '/login';
}
}
return response;
}
3. Logout Flow
async function logout() {
const refreshToken = localStorage.getItem('refresh_token');
// Revoke refresh token on server
await fetch('/api/auth/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
// Clear local storage
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
// Redirect to login
window.location.href = '/login';
}
Environment Configuration
Add to .env:
# JWT Configuration
JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRES_IN=1d
# Refresh Token Configuration
REFRESH_TOKEN_EXPIRY_DAYS=7
File Structure
src/modules/auth/
├── entities/
│ └── refresh-token.entity.ts # RefreshToken entity
├── repositories/
│ └── refresh-token.repository.ts # Database operations
├── services/
│ ├── refresh-token.service.ts # Token generation & validation
│ └── token-cleanup.service.ts # Background cleanup task (optional)
├── dto/
│ ├── refresh-token.dto.ts # RefreshTokenDto
│ └── auth-response.dto.ts # Updated with refresh_token field
├── auth.service.ts # Updated with refresh logic
├── auth.controller.ts # New endpoints added
└── auth.module.ts # Updated with new providers
src/database/migrations/
└── 1736519000000-CreateRefreshTokensTable.ts
Security Best Practices
1. Token Storage
- Frontend: Store in httpOnly cookies (most secure) or localStorage (easier but less secure)
- Mobile: Use secure storage (Keychain on iOS, Keystore on Android)
- Never: Store in plain text files or expose in URLs
2. Token Rotation
- Always implemented by default
- Old tokens are automatically revoked
- Prevents token reuse attacks
3. HTTPS Only
- Always use HTTPS in production
- Never send tokens over HTTP
4. Token Expiration
- Access tokens: Short-lived (1 day)
- Refresh tokens: Long-lived (7 days)
- Adjust based on your security requirements
5. Rate Limiting
Add rate limiting to refresh endpoint:
@Throttle(5, 60) // 5 requests per minute
@Post('refresh')
async refreshToken(@Body() dto: RefreshTokenDto) { ... }
Migration
Run the migration to create the refresh_tokens table:
npm run migration:run
To revert:
npm run migration:revert
Testing
Manual Testing with cURL
1. Login:
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@retailpos.com","password":"Admin123!"}'
2. Refresh Token:
curl -X POST http://localhost:3000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<your-refresh-token>"}'
3. Logout:
curl -X POST http://localhost:3000/api/auth/logout \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<your-refresh-token>"}'
4. Revoke All Tokens:
curl -X POST http://localhost:3000/api/auth/revoke-all \
-H "Authorization: Bearer <your-access-token>"
Monitoring and Maintenance
1. Token Cleanup
The system automatically cleans up:
- Expired tokens (expiresAt < now)
- Revoked tokens (isRevoked = true)
- Old tokens (createdAt > 30 days ago)
2. Monitoring Queries
-- Count active refresh tokens
SELECT COUNT(*) FROM refresh_tokens
WHERE isRevoked = false AND expiresAt > NOW();
-- Count tokens per user
SELECT userId, COUNT(*) as token_count
FROM refresh_tokens
WHERE isRevoked = false
GROUP BY userId;
-- Find expired tokens
SELECT COUNT(*) FROM refresh_tokens
WHERE expiresAt < NOW();
3. Manual Cleanup
# Connect to database
psql -h localhost -U postgres -d retail_pos
# Delete expired tokens
DELETE FROM refresh_tokens WHERE "expiresAt" < NOW();
# Delete revoked tokens
DELETE FROM refresh_tokens WHERE "isRevoked" = true;
Troubleshooting
Issue: "Invalid refresh token"
Causes:
- Token has been revoked
- Token has expired
- Token doesn't exist in database
- User account is inactive
Solution: User needs to login again
Issue: Token rotation not working
Check:
- Ensure old token is being revoked in
refreshAccessToken() - Verify database transaction is committing
- Check logs for errors
Issue: Too many tokens in database
Solution:
- Enable automatic cleanup (TokenCleanupService)
- Run manual cleanup
- Reduce REFRESH_TOKEN_EXPIRY_DAYS
Future Enhancements
- Device Tracking: Track which device/browser each token belongs to
- Token Family: Link related tokens to detect token theft
- Geolocation: Track login locations for security
- Email Notifications: Alert users of new logins
- Admin Dashboard: View and manage user sessions
- Token Reuse Detection: Detect and respond to token replay attacks