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
| Requirement | Priority |
|---|---|
| Stateless authentication for horizontal scaling | High |
| Secure token storage | High |
| Token refresh without re-entering credentials | High |
| Session invalidation (logout) | High |
| RBAC integration | High |
| Mobile app compatibility | Low |
| SSO/LDAP integration ready | Low |
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:
- Security: Short access tokens (30min) limit exposure if compromised
- UX: Refresh tokens (30 days) avoid frequent re-login
- Scalability: Most requests validate JWT locally (no Valkey call)
- Control: Can invalidate all tokens on password change or security event
- 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 daysSecurity 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();
}
}