416 lines
11 KiB
Markdown
416 lines
11 KiB
Markdown
# 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)
|