HyperionDocs

ADR-003: Authentication Strategy

JWT-based authentication with Valkey caching for NPP Portal

ADR-003: Authentication Strategy

Status

  • Status: Accepted
  • Date: 2025-09-25
  • Decision Makers: Development Team

Context

The NPP Portal needs authentication to:

  • Protect user data and activity records
  • Support role-based access (GUEST, TEACHER, DIRECTOR, ADMIN)
  • Enable stateless API for scalability
  • Provide secure token refresh without frequent re-login
  • Support potential mobile app clients in the future

Requirements

RequirementPriority
Stateless authentication for horizontal scalingHigh
Secure token storageHigh
Token refresh without re-entering credentialsHigh
Session invalidation (logout)High
RBAC integrationHigh
Mobile app compatibilityLow
SSO/LDAP integration readyLow

Security Considerations

  • Faculty profiles contain personal data (personal cell-phone number)
  • Public endpoints must not leak private information
  • Tokens must be invalidable on logout/password change

Decision

We will implement JWT-based authentication with a hybrid validation strategy:

  • Access Token: Short-lived JWT (30 minutes), validated locally
  • Refresh Token: Long-lived (7 days), stored in Valkey
  • Token Blacklist: Valkey-based for immediate invalidation

Rationale

Considered Options

1. Session-Based (Server-Side Sessions)

  • Pros: Simple, immediate invalidation, familiar pattern
  • Cons:
    • Requires sticky sessions or shared session store
    • Not suitable for mobile apps
    • Scales poorly without Valkey anyway

2. JWT Only (Stateless)

  • Pros: Truly stateless, no server-side storage, mobile-friendly
  • Cons:
    • Cannot invalidate tokens before expiry
    • Logout requires waiting for token expiry
    • Security risk if token is compromised

3. JWT + Valkey (Chosen)

  • Pros:
    • Short access tokens reduce compromise window
    • Valkey enables immediate logout/invalidation
    • Stateless validation for most requests
    • Refresh tokens enable long sessions
  • Cons:
    • Valkey dependency
    • Slightly more complex implementation

4. OAuth2 with External Provider (leaving for future improvement)

  • Pros: Delegated authentication, SSO support
  • Cons:
    • External dependency
    • Complex setup for simple use case
    • May not support university accounts

Chosen: Option 3 - JWT + Valkey Hybrid

Reasoning:

  1. Security: Short access tokens (30min) limit exposure if compromised
  2. UX: Refresh tokens (30 days) avoid frequent re-login
  3. Scalability: Most requests validate JWT locally (no Valkey call)
  4. Control: Can invalidate all tokens on password change or security event
  5. Future-ready: Pattern supports mobile apps and API consumers

Token Design

Access Token (JWT)

{
  "sub": "user-uuid",
  "iat": 1706745600,
  "exp": 1706747400,
  "roles": ["TEACHER"],
  "userId": 123,
  "departments": [1, 5]
}
  • Lifetime: 30 minutes
  • Validation: Local signature check (no Valkey)
  • Contains: User ID, roles, department IDs for authorization
  • Algorithm: RS256 (asymmetric for microservices readiness)

Refresh Token

{
  "tokenId": "uuid",
  "userId": 123,
  "createdAt": "2026-01-31T10:00:00Z",
  "expiresAt": "2026-03-02T10:00:00Z"
}
  • Lifetime: 7 days
  • Storage: Valkey with TTL
  • Rotation: New refresh token issued on each refresh
  • Invalidation: Deleted from Valkey on logout

Consequences

Positive

  • Horizontal scaling without session affinity
  • Immediate logout capability via Valkey
  • Mobile app ready (token-based)
  • Clear separation of short-term (access) and long-term (refresh) auth
  • Password change invalidates all sessions

Negative

  • Valkey becomes critical dependency for refresh/logout
  • Token size larger than session ID (JWT overhead)
  • Clock synchronization important for token validation

Neutral

  • Team needs to understand JWT security best practices
  • Frontend must handle token refresh transparently

Implementation Notes

Token Flow

1. Login: POST /auth/login
   → Validate credentials
   → Generate access + refresh tokens
   → Store refresh token in Valkey
   → Return both tokens to client

2. API Call: GET /api/users/me
   → Validate access token signature
   → Extract user from token
   → Process request

3. Token Refresh: POST /auth/refresh
   → Validate refresh token exists in Valkey
   → Delete old refresh token
   → Generate new access + refresh tokens
   → Store new refresh token in Valkey
   → Return both tokens

4. Logout: POST /auth/logout
   → Delete refresh token from Valkey
   → (Access token expires naturally)

Valkey Keys

refresh_token:{tokenId} → {userId, createdAt}  TTL: 7 days
user_tokens:{userId} → Set of active tokenIds   TTL: 7 days

Security Measures

  • HTTPS only: Tokens transmitted only over TLS
  • HttpOnly cookies: Option for web (prevents XSS access)
  • Token rotation: New refresh token on each refresh
  • Rate limiting: Max 5 refresh attempts per minute
  • Audit logging: All auth events logged

Spring Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

References