Skip to main content

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:

  1. CSRF state parameter โ€” PkceHelper generates a 32-byte random state alongside the PKCE parameters. It is included in the authorization redirect and stored in the session. On callback, SmartCallbackController verifies the returned state using constant-time comparison (MessageDigest.isEqual) to prevent timing attacks.

  2. 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:

ClaimCheck
algMust be RS256 โ€” rejects alg: none and symmetric algorithms
issMust match the iss from the SMART discovery document
audMust contain the application's client_id
expMust be in the future
nonceMust match the nonce stored in session
SignatureFetched from jwks_uri in the discovery document

Next: Session Lifecycle โ†’