add refresh token
This commit is contained in:
415
REFRESH_TOKEN_IMPLEMENTATION.md
Normal file
415
REFRESH_TOKEN_IMPLEMENTATION.md
Normal 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)
|
||||
Reference in New Issue
Block a user