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

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

  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

// 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

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

  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.