Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,631 @@
# Token Refresh API Implementation
This document describes the complete implementation of the token refresh feature for the MaplePress frontend, integrated with the MaplePress backend API.
## Overview
The token refresh feature allows users to obtain new access tokens without requiring them to log in again. This maintains seamless user sessions by automatically refreshing expired tokens in the background.
## Backend API Endpoint
**Endpoint**: `POST /api/v1/refresh`
**Authentication**: None required (public endpoint, but requires valid refresh token)
**Documentation**: `/cloud/maplepress-backend/docs/API.md` (lines 230-324)
### Request Structure
```json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
### Response Structure
```json
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"user_email": "john@example.com",
"user_name": "John Doe",
"user_role": "user",
"tenant_id": "650e8400-e29b-41d4-a716-446655440000",
"session_id": "750e8400-e29b-41d4-a716-446655440000",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"access_expiry": "2024-10-24T12:30:00Z",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_expiry": "2024-10-31T00:15:00Z",
"refreshed_at": "2024-10-24T12:15:00Z"
}
```
### Key Features
- **Token Rotation**: Both access and refresh tokens are regenerated on each refresh
- **Old Token Invalidation**: Previous tokens become invalid immediately
- **Session Validation**: Validates that the session still exists and is active
- **Automatic Expiry**: Access tokens expire after 15 minutes, refresh tokens after 7 days
## Frontend Implementation
### 1. RefreshTokenService (`src/services/API/RefreshTokenService.js`)
Handles direct communication with the backend refresh API.
**Key Features:**
- Request validation (required refresh token)
- Request body formatting (snake_case for backend)
- Response transformation (camelCase for frontend)
- User-friendly error message mapping
- Helper functions for token expiry checking
**Methods:**
#### `refreshToken(refreshToken)`
Main refresh method that calls the backend API.
```javascript
import RefreshTokenService from './services/API/RefreshTokenService';
const response = await RefreshTokenService.refreshToken("eyJhbGci...");
```
**Returns:**
```javascript
{
userId: string,
userEmail: string,
userName: string,
userRole: string,
tenantId: string,
sessionId: string,
accessToken: string,
accessExpiry: Date,
refreshToken: string,
refreshExpiry: Date,
refreshedAt: Date
}
```
#### `shouldRefreshToken(accessExpiry, bufferMinutes = 1)`
Checks if token should be refreshed based on expiry time.
```javascript
const needsRefresh = RefreshTokenService.shouldRefreshToken(
new Date('2024-10-24T12:14:30Z'), // Access expiry
1 // Refresh if expiring within 1 minute
);
```
#### `isRefreshTokenValid(refreshExpiry)`
Checks if the refresh token itself is still valid.
```javascript
const isValid = RefreshTokenService.isRefreshTokenValid(
new Date('2024-10-31T00:15:00Z')
);
```
### 2. AuthManager Enhancement (`src/services/Manager/AuthManager.js`)
Updated with complete token refresh functionality and automatic refresh logic.
**New Features:**
- Token refresh on initialization if access token expired
- Prevents duplicate refresh requests with `refreshPromise` tracking
- Automatic token refresh before API requests
- Handles refresh failures gracefully
**New Methods:**
#### `async refreshAccessToken()`
Refreshes the access token using the stored refresh token.
```javascript
const authManager = useAuth().authManager;
try {
await authManager.refreshAccessToken();
console.log("Token refreshed successfully");
} catch (error) {
console.error("Refresh failed:", error);
// User will be logged out automatically
}
```
**Features:**
- Prevents duplicate refresh requests (returns same promise if refresh in progress)
- Validates refresh token exists and is not expired
- Clears session on refresh failure
- Implements token rotation (stores new tokens)
- Preserves tenant name/slug from previous session
#### `shouldRefreshToken(bufferMinutes = 1)`
Checks if the current access token needs refresh.
```javascript
if (authManager.shouldRefreshToken()) {
await authManager.refreshAccessToken();
}
```
#### `async ensureValidToken()`
Automatically refreshes token if needed. Called before API requests.
```javascript
// Automatically called by ApiClient before each request
await authManager.ensureValidToken();
```
### 3. ApiClient Enhancement (`src/services/API/ApiClient.js`)
Updated to automatically refresh tokens before making requests and handle 401 errors.
**Automatic Token Refresh:**
- Calls `ensureValidToken()` before every API request
- Skips refresh for the `/api/v1/refresh` endpoint itself
- Can be disabled per-request with `skipTokenRefresh` option
**401 Error Handling:**
- Automatically attempts token refresh on 401 Unauthorized
- Retries the original request once with new token
- Logs out user if refresh fails
**Enhanced Request Flow:**
```javascript
1. Check if token needs refresh (within 1 minute of expiry)
2. If yes, refresh token proactively
3. Make API request with current token
4. If 401 received, force refresh and retry once
5. If retry fails, throw error and clear session
```
## Data Flow
### Initial Authentication
```
User logs in
Backend returns tokens
AuthManager stores tokens
Access Token: valid for 15 minutes
Refresh Token: valid for 7 days
```
### Proactive Token Refresh (Before Expiry)
```
User makes API request
ApiClient checks token expiry
Token expires in < 1 minute?
Yes: Refresh token proactively
AuthManager.ensureValidToken()
AuthManager.refreshAccessToken()
RefreshTokenService.refreshToken()
POST /api/v1/refresh
Backend validates session
Backend returns new tokens
AuthManager stores new tokens
Continue with original API request
```
### Reactive Token Refresh (After 401 Error)
```
API request made
Backend returns 401 Unauthorized
ApiClient detects 401
Attempt token refresh
Retry original request
Success: Return response
Failure: Clear session and throw error
```
### Token Refresh on App Initialization
```
App starts
AuthManager.initialize()
Load tokens from localStorage
Access token expired?
Yes: Check refresh token
Refresh token valid?
Yes: Refresh access token
No: Clear session
```
## Token Lifecycle
### Token Expiry Timeline
```
Login (00:00)
├─ Access Token: expires at 00:15 (15 minutes)
│ └─ Proactive refresh at 00:14 (1 minute before)
└─ Refresh Token: expires at 7 days later
└─ Requires new login when expired
```
### Session Validation
- **Session Duration**: 14 days of inactivity (backend setting)
- **Access Token**: 15 minutes (can be refreshed)
- **Refresh Token**: 7 days (single-use due to rotation)
If the backend session expires (14 days), token refresh will fail even if the refresh token hasn't expired yet.
## Error Handling
### Error Types
| Error Condition | Response | Frontend Behavior |
|----------------|----------|-------------------|
| Missing refresh token | 400 Bad Request | Clear session, throw error |
| Invalid refresh token | 401 Unauthorized | Clear session, show "Session expired" |
| Expired refresh token | 401 Unauthorized | Clear session, show "Session expired" |
| Session invalidated | 401 Unauthorized | Clear session, show "Session expired or invalidated" |
| Server error | 500 Internal Server Error | Clear session, show generic error |
### Error Messages
The frontend maps backend errors to user-friendly messages:
```javascript
// Backend: "invalid or expired refresh token"
// Frontend: "Your session has expired. Please log in again to continue."
// Backend: "session not found or expired"
// Frontend: "Session has expired or been invalidated. Please log in again."
// Backend: Generic error
// Frontend: "Unable to refresh your session. Please log in again to continue."
```
## Security Features
1. **Token Rotation**: Each refresh generates new tokens, invalidating old ones
2. **Session Validation**: Refresh validates against active backend session
3. **Prevents Token Reuse**: Old tokens become invalid immediately
4. **CWE-613 Prevention**: Session validation prevents token use after logout
5. **Proactive Refresh**: Reduces window of expired token exposure
6. **Duplicate Prevention**: `refreshPromise` tracking prevents race conditions
7. **Automatic Cleanup**: Failed refresh automatically clears session
## Usage Examples
### Manual Token Refresh
```javascript
import { useAuth } from './services/Services';
function MyComponent() {
const { authManager } = useAuth();
const handleManualRefresh = async () => {
try {
await authManager.refreshAccessToken();
console.log("Token refreshed manually");
} catch (error) {
console.error("Manual refresh failed:", error);
// User will be redirected to login
}
};
return <button onClick={handleManualRefresh}>Refresh Session</button>;
}
```
### Checking Token Status
```javascript
const { authManager } = useAuth();
// Check if token needs refresh
if (authManager.shouldRefreshToken()) {
console.log("Token expires soon");
}
// Check if refresh token is valid
const refreshExpiry = authManager.refreshExpiry;
if (RefreshTokenService.isRefreshTokenValid(refreshExpiry)) {
console.log("Refresh token is still valid");
}
```
### Automatic Refresh (Built-in)
```javascript
// Automatic refresh is built into ApiClient
// No manual intervention needed
const { apiClient } = useApi();
// Token will be automatically refreshed if needed
const response = await apiClient.get('/api/v1/some-endpoint');
```
## Testing the Token Refresh Flow
### 1. Prerequisites
- Backend running at `http://localhost:8000`
- Frontend running at `http://localhost:5173`
- User already logged in
### 2. Test Proactive Refresh
```javascript
// In browser console
// 1. Login first
// (Use the login form)
// 2. Check token expiry
const expiry = localStorage.getItem('maplepress_access_expiry');
console.log('Token expires at:', expiry);
// 3. Manually set expiry to 30 seconds from now (for testing)
const newExpiry = new Date(Date.now() + 30000).toISOString();
localStorage.setItem('maplepress_access_expiry', newExpiry);
// 4. Make an API request (will trigger proactive refresh after 30 seconds)
// The ApiClient will automatically refresh before the request
// 5. Check that new token was stored
setTimeout(() => {
const newToken = localStorage.getItem('maplepress_access_token');
console.log('New token:', newToken);
}, 31000);
```
### 3. Test Refresh on Initialization
```bash
# 1. Login to the app
# 2. Open browser DevTools → Application → Local Storage
# 3. Find 'maplepress_access_expiry'
# 4. Change the date to yesterday
# 5. Refresh the page
# 6. Check console logs - should see token refresh attempt
```
### 4. Test 401 Handling
This requires backend cooperation (backend must return 401):
```javascript
// In browser console after login
// 1. Make access token invalid
localStorage.setItem('maplepress_access_token', 'invalid_token');
// 2. Make an authenticated API request
// The ApiClient will receive 401, attempt refresh, and retry
// 3. Check console for refresh logs
```
### 5. Test Refresh Failure
```javascript
// In browser console
// 1. Make refresh token invalid
localStorage.setItem('maplepress_refresh_token', 'invalid_refresh');
// 2. Try to refresh
window.maplePressServices.authManager.refreshAccessToken()
.catch(error => console.log('Expected error:', error));
// 3. Check that session was cleared
window.maplePressServices.authManager.isAuthenticated() // Should be false
```
### 6. Backend Testing with curl
```bash
# 1. Login and get refresh token
curl -X POST http://localhost:8000/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!"
}' | jq -r '.refresh_token'
# 2. Use refresh token to get new tokens
curl -X POST http://localhost:8000/api/v1/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGci..."
}' | jq
# Expected: New access_token and refresh_token
# 3. Try to use old refresh token (should fail)
curl -X POST http://localhost:8000/api/v1/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGci..."
}'
# Expected: 401 Unauthorized
```
## Best Practices
### Implemented Best Practices
**Proactive Refresh**: Tokens are refreshed 1 minute before expiry
**Automatic Refresh**: Built into ApiClient, no manual intervention needed
**Token Rotation**: Both tokens regenerated on refresh
**Session Validation**: Backend validates session on each refresh
**Duplicate Prevention**: Only one refresh request at a time
**Error Handling**: Failed refresh clears session and redirects to login
**Secure Storage**: Tokens stored in localStorage (consider upgrading to httpOnly cookies for production)
### Recommended Enhancements
- [ ] Move to HTTP-only cookies for token storage (more secure than localStorage)
- [ ] Implement refresh token allowlist (backend: store valid refresh tokens)
- [ ] Add refresh token fingerprinting (tie token to device)
- [ ] Implement sliding sessions (extend session on activity)
- [ ] Add token refresh monitoring/metrics
- [ ] Implement token refresh failure notifications
## Troubleshooting
### Token refresh fails immediately after login
**Possible causes:**
1. Clock skew between client and server
2. Token expiry dates incorrect
3. localStorage corruption
**Solution:**
- Clear localStorage and login again
- Check browser console for expiry dates
- Verify system clock is correct
### Token refresh succeeds but returns 401 on next request
**Possible causes:**
1. Token not being stored correctly
2. ApiClient not using updated token
3. Race condition between refresh and request
**Solution:**
- Check localStorage after refresh
- Verify `authManager.getAccessToken()` returns new token
- Check for duplicate refresh requests in network tab
### Session cleared unexpectedly
**Possible causes:**
1. Refresh token expired (after 7 days)
2. Backend session invalidated (after 14 days inactivity)
3. User logged out on another device
4. Backend restarted and cleared sessions
**Solution:**
- Check `maplepress_refresh_expiry` in localStorage
- Verify backend session still exists
- Login again to create new session
### Infinite refresh loop
**Possible causes:**
1. Token expiry dates set incorrectly
2. Backend always returning expired tokens
3. `skipTokenRefresh` not working
**Solution:**
- Check for `/api/v1/refresh` endpoint exclusion in ApiClient
- Verify token expiry dates in localStorage
- Check backend logs for refresh endpoint errors
## Integration with Dashboard
The dashboard automatically benefits from token refresh:
```javascript
// Dashboard.jsx
useEffect(() => {
if (!authManager.isAuthenticated()) {
navigate("/login");
return;
}
// Token will be automatically refreshed if needed
const userData = authManager.getUser();
setUser(userData);
}, [authManager, navigate]);
```
Any API calls made from the dashboard will automatically trigger token refresh if needed.
## Future Enhancements
### Planned Features
- [ ] **Background Refresh Timer**: Periodic refresh check every 5 minutes
- [ ] **Session Extension Prompt**: Ask user to extend session before refresh token expires
- [ ] **Multi-tab Synchronization**: Coordinate token refresh across browser tabs
- [ ] **Refresh Analytics**: Track refresh success/failure rates
- [ ] **Grace Period Handling**: Handle partial network failures gracefully
### Security Improvements
- [ ] **HTTP-only Cookies**: Move from localStorage to secure cookies
- [ ] **Token Binding**: Bind tokens to specific devices/browsers
- [ ] **Refresh Token Allowlist**: Backend maintains list of valid refresh tokens
- [ ] **Token Rotation Detection**: Detect and prevent token replay attacks
- [ ] **Session Fingerprinting**: Additional validation beyond token
## Related Files
### Created Files
```
src/services/API/RefreshTokenService.js
```
### Modified Files
```
src/services/Manager/AuthManager.js
src/services/API/ApiClient.js
```
### Backend Reference Files
```
cloud/maplepress-backend/docs/API.md (lines 230-324)
cloud/maplepress-backend/internal/interface/http/dto/gateway/refresh_dto.go
cloud/maplepress-backend/internal/interface/http/handler/gateway/refresh_handler.go
```
## Related Documentation
- [LOGIN_API.md](./LOGIN_API.md) - Login implementation and token storage
- [REGISTRATION_API.md](./REGISTRATION_API.md) - Registration implementation
- [FRONTEND_ARCHITECTURE.md](./FRONTEND_ARCHITECTURE.md) - Complete architecture guide
- [README.md](./README.md) - Documentation index
- [Backend API Documentation](../../../cloud/maplepress-backend/docs/API.md) - Complete API reference
## Summary
The token refresh implementation provides:
1. **Seamless Sessions**: Users stay logged in without interruption
2. **Automatic Refresh**: No manual intervention required
3. **Proactive Strategy**: Tokens refreshed before expiry
4. **Reactive Fallback**: Handles unexpected 401 errors
5. **Security**: Token rotation prevents reuse
6. **Reliability**: Duplicate prevention and error handling
The implementation follows backend best practices and integrates seamlessly with the existing three-layer service architecture.
---
**Last Updated**: October 30, 2024
**Frontend Version**: 0.0.0
**Documentation Version**: 1.0.0