# 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 ```sql 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**: ```json { "email": "user@example.com", "password": "Password123!" } ``` **Response**: ```json { "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**: ```json { "refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6..." } ``` **Response**: ```json { "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**: ```json { "refreshToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6..." } ``` **Response**: ```json { "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 ``` **Response**: ```json { "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 ```typescript // 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 ```typescript 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 ```typescript 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`: ```bash # 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: ```typescript @Throttle(5, 60) // 5 requests per minute @Post('refresh') async refreshToken(@Body() dto: RefreshTokenDto) { ... } ``` ## Migration Run the migration to create the refresh_tokens table: ```bash npm run migration:run ``` To revert: ```bash npm run migration:revert ``` ## Testing ### Manual Testing with cURL **1. Login**: ```bash curl -X POST http://localhost:3000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@retailpos.com","password":"Admin123!"}' ``` **2. Refresh Token**: ```bash curl -X POST http://localhost:3000/api/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refreshToken":""}' ``` **3. Logout**: ```bash curl -X POST http://localhost:3000/api/auth/logout \ -H "Content-Type: application/json" \ -d '{"refreshToken":""}' ``` **4. Revoke All Tokens**: ```bash curl -X POST http://localhost:3000/api/auth/revoke-all \ -H "Authorization: Bearer " ``` ## 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 ```sql -- 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 ```bash # 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 1. **Device Tracking**: Track which device/browser each token belongs to 2. **Token Family**: Link related tokens to detect token theft 3. **Geolocation**: Track login locations for security 4. **Email Notifications**: Alert users of new logins 5. **Admin Dashboard**: View and manage user sessions 6. **Token Reuse Detection**: Detect and respond to token replay attacks ## References - [NestJS JWT Documentation](https://docs.nestjs.com/security/authentication) - [OWASP Token Storage Best Practices](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html) - [RFC 6749 - OAuth 2.0 Refresh Tokens](https://tools.ietf.org/html/rfc6749#section-1.5)