ut and TTL management for session lifecycle control.
package com.codcompass.identity.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class DistributedSessionConfig {
// Spring Boot auto-configures RedisIndexedSessionRepository
// when spring-session-data-redis is on the classpath
}
The maxInactiveIntervalInSeconds parameter defines idle timeout. Spring Session automatically manages cookie lifecycle, serialization, and Redis key expiration. No custom repository code is required.
Step 2: Implement Dual Authentication Chains
Spring Security 6+ evaluates SecurityFilterChain beans in ascending @Order priority. The first matching request pattern wins. We separate browser routes from API routes to apply distinct session policies.
package com.codcompass.identity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class AuthenticationRoutingConfig {
@Bean
@Order(1)
public SecurityFilterChain browserInteractionChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/portal/**", "/dashboard/**")
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(3)
.maxSessionsPreventsLogin(false))
.formLogin(form -> form
.loginPage("/portal/authenticate")
.permitAll())
.logout(logout -> logout
.logoutUrl("/portal/sign-out")
.invalidateHttpSession(true)
.deleteCookies("SESSION"))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/portal/authenticate").permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain serviceApiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/v2/**", "/internal/events/**")
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(customJwtDecoder())
.jwtAuthenticationConverter(new RoleClaimMapper())))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v2/health").permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable());
return http.build();
}
}
Step 3: Build the JWT Verification Pipeline
The API chain delegates to Spring's OAuth2 resource server support. This replaces manual filter implementation with a standardized decoder pipeline that handles signature verification, expiry checks, and claim extraction.
package com.codcompass.identity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
public class JwtPipelineFactory {
@Bean
public JwtDecoder customJwtDecoder() {
String signingKey = System.getenv("IDENTITY_HMAC_SECRET");
Key hmacKey = new SecretKeySpec(
signingKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
return NimbusJwtDecoder.withSecretKey((javax.crypto.SecretKey) hmacKey)
.macAlgorithm(org.springframework.security.oauth2.jose.jws.MacAlgorithm.HS256)
.jwtValidators(new AudienceAndIssuerValidator())
.build();
}
}
Architecture Rationale
- Separation of Concerns: Browser clients receive
HttpOnly; Secure cookies managed by the container. API clients receive bearer tokens validated in-memory. This eliminates cookie configuration complexity for mobile/third-party integrations while preserving session revocation for web users.
- Redis as Single Source of Truth: Session state lives exclusively in the cache. Password changes, role updates, or administrative suspensions trigger immediate cache key deletion. No token expiry window exists.
- Standardized JWT Pipeline: Using
oauth2ResourceServer instead of custom filters reduces implementation surface area. Spring handles base64 decoding, cryptographic verification, and standard claim validation. Custom validators only enforce business-specific constraints.
- Explicit Ordering:
@Order(1) guarantees browser routes never fall through to the stateless API chain. Request matchers are evaluated sequentially; misordering causes authentication bypass or session leakage.
Pitfall Guide
1. The Blocklist Anti-Pattern
Explanation: Teams adopt JWT for statelessness, then add a Redis blocklist to support revocation. Every request now performs a cache lookup plus signature verification. The system retains JWT parsing overhead while losing its primary architectural benefit.
Fix: If revocation is required, use server-side sessions. If JWT is mandatory, enforce short expiry windows (15 minutes) and implement refresh token rotation with a dedicated revocation store. Never mix stateless verification with synchronous blocklist checks.
2. Under-Sized Symmetric Keys
Explanation: HS256 requires a minimum of 256 bits (32 bytes). Keys shorter than this are vulnerable to brute-force attacks. Many tutorials use 16-character strings or UUIDs, which fail cryptographic strength requirements.
Fix: Generate keys using openssl rand -base64 32 or equivalent CSPRNG. Store in a secrets manager. Validate length at application startup with an explicit assertion.
3. Claim Validation Blind Spots
Explanation: Accepting tokens without verifying iss (issuer) and aud (audience) enables cross-service replay attacks. A token issued for service-alpha can authenticate against service-beta if claims are ignored.
Fix: Configure JwtDecoder with explicit validators. Reject tokens where aud does not match the service identifier. Log and drop requests with mismatched issuers.
4. Unnecessary Asymmetric Key Management
Explanation: RS256 requires public/private key pairs, certificate rotation, and JWK endpoint maintenance. In a monolithic application where a single service issues and verifies tokens, asymmetric cryptography adds operational complexity without security improvement.
Fix: Use HS256 for single-service verification. Reserve RS256 for distributed architectures where an independent authorization server issues tokens consumed by multiple untrusted services.
5. Cookie Security Neglect
Explanation: Session cookies transmitted without Secure, HttpOnly, and SameSite=Strict flags are vulnerable to XSS extraction, CSRF manipulation, and cross-origin leakage.
Fix: Configure server.servlet.session.cookie.secure=true, http-only=true, and same-site=strict in application properties. Never expose session identifiers to client-side JavaScript.
6. Filter Chain Ordering Conflicts
Explanation: Spring Security evaluates chains sequentially. If a stateless API chain is ordered before a session-based web chain, browser requests may bypass authentication or trigger stateless policy violations.
Fix: Assign lower @Order values to more specific matchers. Use securityMatcher() with precise path patterns. Validate chain resolution with integration tests that assert authentication behavior per route.
7. Credential Logging
Explanation: JWTs and session IDs frequently appear in debug logs, error traces, and access metrics. Storing these values violates credential handling policies and creates compliance exposure.
Fix: Implement request sanitization filters that redact Authorization and Cookie headers before logging. Configure log appenders to mask bearer tokens. Audit logging pipelines for accidental credential persistence.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Browser-based SaaS with admin controls | Server-Side Sessions | Immediate revocation required for compliance; Redis lookup latency is negligible | Low (Redis cluster required) |
| Multi-service microservices mesh | Stateless JWT | Services verify identity independently without shared storage or network hops | Medium (Key management overhead) |
| Mobile app + web frontend | Hybrid Architecture | Web requires session revocation; mobile requires bearer token compatibility | Medium (Dual pipeline maintenance) |
| High-frequency internal API (>10k RPS) | Stateless JWT | Eliminates Redis round-trip; sub-millisecond verification meets microsecond budgets | Low (No cache dependency) |
| Third-party API integration | Stateless JWT | External clients cannot manage HttpOnly cookies; standard bearer auth is expected | Low (Standard OAuth2 flow) |
Configuration Template
# application-production.yml
spring:
session:
store-type: redis
timeout: 30m
redis:
namespace: app:identity:sessions
flush-mode: on_save
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 4
server:
servlet:
session:
cookie:
http-only: true
secure: true
same-site: strict
max-age: 1800
security:
jwt:
issuer: https://auth.internal.example.com
audience: api-gateway
secret-length-bits: 256
expiry-minutes: 15
refresh-expiry-hours: 72
Quick Start Guide
- Add Dependencies: Include
spring-boot-starter-security, spring-boot-starter-oauth2-resource-server, and spring-session-data-redis in your build configuration.
- Configure Redis: Set
spring.session.store-type=redis and provide connection parameters. Spring Boot auto-configures the session repository.
- Define Filter Chains: Create two
@Bean methods returning SecurityFilterChain. Assign @Order(1) to session-based routes and @Order(2) to stateless API routes. Use securityMatcher() to isolate path patterns.
- Wire JWT Decoder: Configure
oauth2ResourceServer().jwt() with a JwtDecoder bean. Supply the HMAC secret or JWK URI. Attach custom validators for iss and aud.
- Validate Routing: Execute integration tests targeting
/portal/** and /api/v2/**. Assert that browser routes establish sessions and API routes reject unauthenticated bearer tokens. Verify Redis key creation for active sessions.
Architecting identity verification requires abandoning tutorial defaults and aligning authentication strategy with operational constraints. Sessions provide immediate control with predictable latency. JWTs enable distributed verification with minimal network dependency. The hybrid pattern isolates these strengths, preventing architectural drift and ensuring compliance without sacrificing performance.