Various Improvements.
- Testing Coverage - Image Handling - Session Handling - Library Switching
This commit is contained in:
269
REFRESH_TOKEN_IMPLEMENTATION.md
Normal file
269
REFRESH_TOKEN_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user