Security Model
Spring Security filter chainโ
SecurityConfig defines a single SecurityFilterChain. Order of filters:
Request
โ
โโ CsrfFilter (Double-Submit Cookie)
โโ SessionManagementFilter (fixation protection: new session ID on login)
โโ SmartSecurityFilter (custom โ checks SmartLaunchContext in session)
โโ TokenRefreshFilter (custom โ proactive refresh on /api/**)
โ
โโ DispatcherServlet
Route authorisationโ
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/launch", "/callback", "/error",
"/actuator/health", "/css/**", "/js/**").permitAll()
.requestMatchers("/api/**", "/dashboard", "/patient/**").authenticated()
.anyRequest().denyAll() // explicit deny-all fallback
);
denyAll() as a fallback is a defence-in-depth measure โ any route not explicitly matched is denied rather than permitted.
PKCE (Proof Key for Code Exchange)โ
RFC 7636 S256 with a 96-byte verifier (768-bit entropy โ well above the spec minimum of 32 bytes):
// PkceHelper.generate()
byte[] verifierBytes = new byte[96];
new SecureRandom().nextBytes(verifierBytes);
String codeVerifier = Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(verifierBytes);
// S256 challenge
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
String codeChallenge = Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(digest);
The verifier is stored in PkceParameters in the HTTP session. It is sent in the token exchange request. Epic verifies that SHA-256(code_verifier) == code_challenge that was sent in the authorization request.
CSRF protectionโ
Two layers:
-
CSRF
stateparameter โPkceHelpergenerates a 32-byte randomstatealongside the PKCE parameters. It is included in the authorization redirect and stored in the session. On callback,SmartCallbackControllerverifies the returnedstateusing constant-time comparison (MessageDigest.isEqual) to prevent timing attacks. -
Spring Security CSRF (Double-Submit Cookie) โ enabled for all state-mutating requests within the application. The CSRF token is embedded in Thymeleaf forms via
th:action.
Session managementโ
http.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionFixation().newSession() // new session ID on each auth
.maximumSessions(3) // max 3 concurrent sessions per user
);
Session fixation protection: Spring replaces the session ID after authentication. A stolen pre-auth session ID cannot be used post-auth.
Log injection sanitisationโ
Patient and clinician IDs extracted from JWT claims are sanitised before logging:
private static String sanitise(String value) {
if (value == null) return "null";
return value.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
This prevents log forging via crafted claim values.
id_token validationโ
IdTokenValidator validates every claim:
| Claim | Check |
|---|---|
alg | Must be RS256 โ rejects alg: none and symmetric algorithms |
iss | Must match the iss from the SMART discovery document |
aud | Must contain the application's client_id |
exp | Must be in the future |
nonce | Must match the nonce stored in session |
| Signature | Fetched from jwks_uri in the discovery document |
Next: Session Lifecycle โ