Files
storycove/REFRESH_TOKEN_IMPLEMENTATION.md
Stefan Hardegger 30c0132a92 Various Improvements.
- Testing Coverage
- Image Handling
- Session Handling
- Library Switching
2025-10-20 08:24:29 +02:00

270 lines
8.4 KiB
Markdown

# Refresh Token Implementation
## Overview
This document describes the refresh token functionality implemented for StoryCove, allowing users to stay authenticated for up to 2 weeks with automatic token refresh.
## Architecture
### Token Types
1. **Access Token (JWT)**
- Lifetime: 24 hours
- Stored in: httpOnly cookie + localStorage
- Used for: API authentication
- Format: JWT with subject and libraryId claims
2. **Refresh Token**
- Lifetime: 14 days (2 weeks)
- Stored in: httpOnly cookie + database
- Used for: Generating new access tokens
- Format: Secure random 256-bit token (Base64 encoded)
### Token Flow
1. **Login**
- User provides password
- Backend validates password
- Backend generates both access token and refresh token
- Both tokens sent as httpOnly cookies
- Access token also returned in response body for localStorage
2. **API Request**
- Frontend sends access token via Authorization header and cookie
- Backend validates access token
- If valid: Request proceeds
- If expired: Frontend attempts token refresh
3. **Token Refresh**
- Frontend detects 401/403 response
- Frontend automatically calls `/api/auth/refresh`
- Backend validates refresh token from cookie
- If valid: New access token generated and returned
- If invalid/expired: User redirected to login
4. **Logout**
- Frontend calls `/api/auth/logout`
- Backend revokes refresh token in database
- Both cookies cleared
- User redirected to login page
## Backend Implementation
### New Files
1. **`RefreshToken.java`** - Entity class
- Fields: id, token, expiresAt, createdAt, revokedAt, libraryId, userAgent, ipAddress
- Helper methods: isExpired(), isRevoked(), isValid()
2. **`RefreshTokenRepository.java`** - Repository interface
- findByToken(String)
- deleteExpiredTokens(LocalDateTime)
- revokeAllByLibraryId(String, LocalDateTime)
- revokeAll(LocalDateTime)
3. **`RefreshTokenService.java`** - Service class
- createRefreshToken(libraryId, userAgent, ipAddress)
- verifyRefreshToken(token)
- revokeToken(token)
- revokeAllByLibraryId(libraryId)
- cleanupExpiredTokens() - Scheduled daily at 3 AM
### Modified Files
1. **`JwtUtil.java`**
- Added `refreshExpiration` property (14 days)
- Added `generateRefreshToken()` method
- Added `getRefreshExpirationMs()` method
2. **`AuthController.java`**
- Updated `/login` endpoint to create and return refresh token
- Added `/refresh` endpoint to handle token refresh
- Updated `/logout` endpoint to revoke refresh token
- Added helper methods: `getRefreshTokenFromCookies()`, `getClientIpAddress()`
3. **`SecurityConfig.java`**
- Added `/api/auth/refresh` to public endpoints
4. **`application.yml`**
- Added `storycove.jwt.refresh-expiration: 1209600000` (14 days)
## Frontend Implementation
### Modified Files
1. **`api.ts`**
- Added automatic token refresh logic in response interceptor
- Added request queuing during token refresh
- Prevents multiple simultaneous refresh attempts
- Automatically retries failed requests after refresh
### Token Refresh Logic
```typescript
// On 401/403 response:
1. Check if already retrying -> if yes, queue request
2. Check if refresh/login endpoint -> if yes, logout
3. Attempt token refresh via /api/auth/refresh
4. If successful:
- Update localStorage with new token
- Retry original request
- Process queued requests
5. If failed:
- Clear token
- Redirect to login
- Reject queued requests
```
## Security Features
1. **httpOnly Cookies**: Prevents XSS attacks
2. **Token Revocation**: Refresh tokens can be revoked
3. **Database Storage**: Refresh tokens stored server-side
4. **Expiration Tracking**: Tokens have strict expiration dates
5. **IP & User Agent Tracking**: Stored for security auditing
6. **Library Isolation**: Tokens scoped to specific library
## Database Schema
```sql
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY,
token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
library_id VARCHAR(255),
user_agent VARCHAR(255) NOT NULL,
ip_address VARCHAR(255) NOT NULL
);
CREATE INDEX idx_refresh_token ON refresh_tokens(token);
CREATE INDEX idx_expires_at ON refresh_tokens(expires_at);
```
## Configuration
### Backend (`application.yml`)
```yaml
storycove:
jwt:
expiration: 86400000 # 24 hours (access token)
refresh-expiration: 1209600000 # 14 days (refresh token)
```
### Environment Variables
No new environment variables required. Existing `JWT_SECRET` is used.
## Testing
Comprehensive test suite in `RefreshTokenServiceTest.java`:
- Token creation
- Token validation
- Expired token handling
- Revoked token handling
- Token revocation
- Cleanup operations
Run tests:
```bash
cd backend
mvn test -Dtest=RefreshTokenServiceTest
```
## Maintenance
### Automated Cleanup
Expired tokens are automatically cleaned up daily at 3 AM via scheduled task in `RefreshTokenService.cleanupExpiredTokens()`.
### Manual Revocation
```java
// Revoke all tokens for a library
refreshTokenService.revokeAllByLibraryId("library-id");
// Revoke all tokens (logout all users)
refreshTokenService.revokeAll();
```
## User Experience
1. **Seamless Authentication**: Users stay logged in for 2 weeks
2. **Automatic Refresh**: Token refresh happens transparently
3. **No Interruptions**: API calls succeed even when access token expires
4. **Backend Restart**: Users must re-login (JWT secret rotates on startup)
5. **Cross-Device Library Switching**: Automatic library switching when using different devices with different libraries
## Cross-Device Library Switching
### Feature Overview
The system automatically detects and switches libraries when you use different devices authenticated to different libraries. This ensures you always see the correct library's data.
### How It Works
**Scenario 1: Active Access Token (within 24 hours)**
1. Request comes in with valid JWT access token
2. `JwtAuthenticationFilter` extracts `libraryId` from token
3. Compares with `currentLibraryId` in backend
4. **If different**: Automatically switches to token's library
5. **If same**: Early return (no overhead, just string comparison)
6. Request proceeds with correct library
**Scenario 2: Token Refresh (after 24 hours)**
1. Access token expired, refresh token still valid
2. `/api/auth/refresh` endpoint validates refresh token
3. Extracts `libraryId` from refresh token
4. Compares with `currentLibraryId` in backend
5. **If different**: Automatically switches to token's library
6. **If same**: Early return (no overhead)
7. Generates new access token with correct `libraryId`
**Scenario 3: After Backend Restart**
1. `currentLibraryId` is null (no active library)
2. First request with any token automatically switches to that token's library
3. Subsequent requests use early return optimization
### Performance
**When libraries match** (most common case):
- Simple string comparison: `libraryId.equals(currentLibraryId)`
- Immediate return - zero overhead
- No datasource changes, no reindexing
**When libraries differ** (switching devices):
- Synchronized library switch
- Datasource routing updated instantly
- Solr reindex runs asynchronously (doesn't block request)
- Takes 2-3 seconds in background
### Edge Cases
**Multi-device simultaneous use:**
- If two devices with different libraries are used simultaneously
- Last request "wins" and switches backend to its library
- Not recommended but handled gracefully
- Each device corrects itself on next request
**Library doesn't exist:**
- If token contains invalid `libraryId`
- Library switch fails with error
- Request is rejected with 500 error
- User must re-login with valid credentials
## Future Enhancements
Potential improvements:
1. Persistent JWT secret (survive backend restarts)
2. Sliding refresh token expiration (extend on use)
3. Multiple device management (view/revoke sessions)
4. Configurable token lifetimes via environment variables
5. Token rotation (new refresh token on each use)
6. Thread-local library context for true stateless operation
## Summary
The refresh token implementation provides a robust, secure authentication system that balances user convenience (2-week sessions) with security (short-lived access tokens, automatic refresh). The implementation follows industry best practices and provides a solid foundation for future enhancements.