8.4 KiB
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
-
Access Token (JWT)
- Lifetime: 24 hours
- Stored in: httpOnly cookie + localStorage
- Used for: API authentication
- Format: JWT with subject and libraryId claims
-
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
-
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
-
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
-
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
-
Logout
- Frontend calls
/api/auth/logout - Backend revokes refresh token in database
- Both cookies cleared
- User redirected to login page
- Frontend calls
Backend Implementation
New Files
-
RefreshToken.java- Entity class- Fields: id, token, expiresAt, createdAt, revokedAt, libraryId, userAgent, ipAddress
- Helper methods: isExpired(), isRevoked(), isValid()
-
RefreshTokenRepository.java- Repository interface- findByToken(String)
- deleteExpiredTokens(LocalDateTime)
- revokeAllByLibraryId(String, LocalDateTime)
- revokeAll(LocalDateTime)
-
RefreshTokenService.java- Service class- createRefreshToken(libraryId, userAgent, ipAddress)
- verifyRefreshToken(token)
- revokeToken(token)
- revokeAllByLibraryId(libraryId)
- cleanupExpiredTokens() - Scheduled daily at 3 AM
Modified Files
-
JwtUtil.java- Added
refreshExpirationproperty (14 days) - Added
generateRefreshToken()method - Added
getRefreshExpirationMs()method
- Added
-
AuthController.java- Updated
/loginendpoint to create and return refresh token - Added
/refreshendpoint to handle token refresh - Updated
/logoutendpoint to revoke refresh token - Added helper methods:
getRefreshTokenFromCookies(),getClientIpAddress()
- Updated
-
SecurityConfig.java- Added
/api/auth/refreshto public endpoints
- Added
-
application.yml- Added
storycove.jwt.refresh-expiration: 1209600000(14 days)
- Added
Frontend Implementation
Modified Files
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
// 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
- httpOnly Cookies: Prevents XSS attacks
- Token Revocation: Refresh tokens can be revoked
- Database Storage: Refresh tokens stored server-side
- Expiration Tracking: Tokens have strict expiration dates
- IP & User Agent Tracking: Stored for security auditing
- Library Isolation: Tokens scoped to specific library
Database Schema
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)
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:
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
// Revoke all tokens for a library
refreshTokenService.revokeAllByLibraryId("library-id");
// Revoke all tokens (logout all users)
refreshTokenService.revokeAll();
User Experience
- Seamless Authentication: Users stay logged in for 2 weeks
- Automatic Refresh: Token refresh happens transparently
- No Interruptions: API calls succeed even when access token expires
- Backend Restart: Users must re-login (JWT secret rotates on startup)
- 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)
- Request comes in with valid JWT access token
JwtAuthenticationFilterextractslibraryIdfrom token- Compares with
currentLibraryIdin backend - If different: Automatically switches to token's library
- If same: Early return (no overhead, just string comparison)
- Request proceeds with correct library
Scenario 2: Token Refresh (after 24 hours)
- Access token expired, refresh token still valid
/api/auth/refreshendpoint validates refresh token- Extracts
libraryIdfrom refresh token - Compares with
currentLibraryIdin backend - If different: Automatically switches to token's library
- If same: Early return (no overhead)
- Generates new access token with correct
libraryId
Scenario 3: After Backend Restart
currentLibraryIdis null (no active library)- First request with any token automatically switches to that token's library
- 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:
- Persistent JWT secret (survive backend restarts)
- Sliding refresh token expiration (extend on use)
- Multiple device management (view/revoke sessions)
- Configurable token lifetimes via environment variables
- Token rotation (new refresh token on each use)
- 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.