270 lines
8.4 KiB
Markdown
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.
|