add refresh token

This commit is contained in:
Phuoc Nguyen
2025-10-21 16:30:18 +07:00
parent d316362f41
commit 71f0447af7
17 changed files with 2074 additions and 18 deletions

View File

@@ -0,0 +1,415 @@
# 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 <access_token>
```
**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":"<your-refresh-token>"}'
```
**3. Logout**:
```bash
curl -X POST http://localhost:3000/api/auth/logout \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<your-refresh-token>"}'
```
**4. Revoke All Tokens**:
```bash
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
```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)