Security Implementation
1. Overview
This document describes the implementation details for security in the Registration Portal. The gateway supports two authentication methods—hash-based and OIDC—both resulting in a backend-issued JWT stored server-side in HTTP sessions.
For security requirements, see Security Requirements. For security architecture, see Security Architecture.
2. Key Components
The security implementation consists of several key components:
| Component | Purpose | Package |
|---|---|---|
|
Shared session attribute names |
|
|
Hash-based authentication endpoint |
|
|
OIDC success handler with token exchange |
|
|
Calls backend token exchange endpoint |
|
|
Injects JWT into backend requests |
|
|
Spring Security configuration |
|
3. Session Attribute Management
3.1. Shared Constants
All components use shared session attribute names defined in SessionConstants:
public final class SessionConstants {
public static final String BACKEND_JWT = "BACKEND_JWT";
public static final String BACKEND_JWT_EXPIRY = "BACKEND_JWT_EXPIRY";
}
-
BACKEND_JWT- The backend-issued JWT token (set by both hash and OIDC flows) -
BACKEND_JWT_EXPIRY- Token expiry timestamp (OIDC only; null for hash-based)
4. Hash-Based Authentication
4.2. AuthenticationResource Implementation
The AuthenticationResource handles hash-based authentication from external systems:
Key Implementation Points:
-
Endpoint:
POST /api/auth/external-login -
Extracts
registrationSystemIdfromTenantContext(populated by TenantResolutionFilter) -
Calls backend via
AdminServiceAuthClientwith X-API-KEY authentication -
Stores JWT in session using
SessionConstants.BACKEND_JWT -
Does NOT set
BACKEND_JWT_EXPIRY(hash-based tokens have no client-side expiry tracking) -
Returns
200 OKwith empty body—JWT never sent to frontend
5. OIDC Authentication
5.2. BackendTokenExchangeSuccessHandler
Extends SavedRequestAwareAuthenticationSuccessHandler to exchange IdP tokens for backend JWT immediately after successful OIDC authentication.
Key Implementation Points:
-
Invoked by Spring Security after successful OAuth2 login
-
Loads
OAuth2AuthorizedClientfromOAuth2AuthorizedClientService -
Extracts IdP access token and ID token (if OIDC)
-
Calls
BackendTokenExchangeClientto exchange for backend JWT -
Stores both token and expiry in session
-
Delegates to parent for redirect handling (preserves original request)
5.3. BackendTokenExchangeClient
Service component that calls the backend token exchange endpoint.
Key Implementation Points:
-
Endpoint called:
POST /auth/token-exchange -
Uses RestTemplate with X-API-KEY header for service authentication
-
Request payload:
{accessToken, idToken, clientRegistrationId} -
Response payload:
{token, expiresAt} -
Throws
BackendTokenExchangeExceptionon failure
5.4. OAuth2AuthorizedClientManager Configuration
Configure refresh token support in SecurityConfiguration:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository
) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken() // Enable automatic token refresh
.build();
DefaultOAuth2AuthorizedClientManager manager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository
);
manager.setAuthorizedClientProvider(authorizedClientProvider);
return manager;
}
The refreshToken() provider enables automatic IdP token refresh when the access token expires.
6. JWT Relay Filter
6.1. AdminServiceJwtRelayFilter
The filter intercepts requests to /services/admin-service/** and injects the backend JWT from session.
Filter Logic:
6.2. Token Expiry Handling
The filter handles token expiry differently based on authentication type:
| Scenario | Detection | Action |
|---|---|---|
Hash-based, no expiry info |
|
Relay token as-is; backend handles expiry |
OIDC, token valid |
Expiry > now + 30 seconds |
Relay token |
OIDC, token expiring soon |
Expiry ≤ now + 30 seconds |
Refresh via IdP, then relay new token |
OIDC, refresh fails |
Exception or null result |
Redirect to IdP login |
No OIDC auth, token expired |
Not |
Clear session, pass through without header |
6.3. OIDC Token Refresh
When the backend JWT is expired and OIDC authentication is present:
-
Get OAuth2AuthorizedClient - Use
OAuth2AuthorizedClientManagerwhich automatically refreshes if needed -
Check IdP token validity - If still expired after manager call, redirect to IdP
-
Exchange for new backend JWT - Call
BackendTokenExchangeClient -
Update session - Store new token and expiry
-
Relay request - Continue with fresh token
7. Security Configuration
7.1. SecurityFilterChain Configuration
Key configuration points in SecurityConfiguration:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc,
BackendTokenExchangeSuccessHandler successHandler) throws Exception {
http
.authorizeHttpRequests(authz -> authz
// Gateway endpoints that require authentication
.requestMatchers(mvc.pattern("/api/**")).authenticated()
// Proxied requests - let backend decide (gateway just relays)
.requestMatchers(mvc.pattern("/services/**")).permitAll()
// ...
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/")
.userInfoEndpoint(userInfo -> userInfo.oidcUserService(this.oidcUserService()))
.successHandler(successHandler) // Token exchange handler
)
.oauth2Client(withDefaults());
return http.build();
}
Important: /services/** is permitAll() because:
-
The gateway should not enforce authentication for proxied requests
-
The backend is responsible for all authorization decisions
-
Public endpoints are handled by backend returning appropriate responses
-
Hash-based and OIDC users with valid tokens get JWT relayed
-
Guest/anonymous users get no Authorization header; backend decides response
8. Session Cookie Configuration
8.1. Production Configuration
server:
forward-headers-strategy: native # Trust X-Forwarded-* headers
servlet:
session:
cookie:
name: JSESSIONID
http-only: true # Prevents JavaScript access
secure: true # Cookie only sent over HTTPS
same-site: strict # CSRF protection
timeout: 30m
8.2. Development Configuration
# application-dev.yml
server:
servlet:
session:
cookie:
secure: false # Allow over HTTP during development
Never set secure: false in production environments.
|
8.3. Kubernetes and Reverse Proxy Considerations
In production Kubernetes deployments, TLS is typically terminated at the ingress controller:
This works correctly with secure: true cookies because:
-
NGINX ingress adds
X-Forwarded-Proto: httpsheader -
With
forward-headers-strategy: native, Spring Boot treats request as HTTPS -
Spring Security sets
secureflag on cookies -
Browser receives cookies over HTTPS and saves them correctly
9. Backend Token Exchange Endpoint
The backend must implement a token exchange endpoint:
Endpoint: POST /auth/token-exchange
Request:
{
"accessToken": "eyJhbG...",
"idToken": "eyJhbG...",
"clientRegistrationId": "keycloak"
}
Response:
{
"token": "eyJhbG...",
"expiresAt": "2024-01-15T10:30:00Z"
}
Backend Responsibilities:
-
Validate IdP token (verify signature, issuer, audience)
-
Extract user identity from token claims
-
Look up or create OrgUser based on IdP subject claim and RegistrationSystem
-
Mint backend JWT with standard claims (orgId, personId, etc.)
-
Return token with expiry timestamp
10. Why Sessions Instead of Bearer Tokens?
The choice to use session cookies rather than having Angular manage bearer tokens directly:
| Aspect | Session Cookies | Angular-managed Tokens |
|---|---|---|
Storage |
Server-side (HttpSession) |
Client-side (localStorage/sessionStorage) |
XSS Vulnerability |
Low - |
High - Tokens accessible to malicious scripts |
CSRF Vulnerability |
Mitigated with |
N/A (but XSS can steal tokens) |
Token Refresh |
Handled server-side transparently |
Requires client-side refresh logic |
Front-end Complexity |
Minimal - browser handles cookies |
Requires interceptors, storage, refresh logic |
11. Cluster Considerations
For clustered deployments:
Option 1: Distributed Sessions
Use Spring Session with Redis:
spring:
session:
store-type: redis
redis:
host: redis-server
port: 6379
Option 2: Sticky Sessions
Configure load balancer for session affinity:
-
Kubernetes: Use sessionAffinity on Service
-
NGINX: Use
ip_hashor sticky cookie
Backend Remains Stateless:
-
Backend only validates JWT tokens
-
No session state required in backend
-
Horizontal scaling without session concerns
12. Testing Configuration
For integration tests, provide mock implementations:
@TestConfiguration
public class TestSecurityConfiguration {
@Bean
@Primary
OAuth2AuthorizedClientManager authorizedClientManager() {
return mock(OAuth2AuthorizedClientManager.class);
}
@Bean
@Primary
BackendTokenExchangeClient backendTokenExchangeClient() {
BackendTokenExchangeClient mockClient = mock(BackendTokenExchangeClient.class);
when(mockClient.exchangeForBackendJwt(anyString(), anyString(), anyString()))
.thenReturn(new BackendTokenResponse("test-jwt", Instant.now().plusSeconds(3600)));
return mockClient;
}
}