Skip to main content

Security Filter Chains

AuthorizationServerConfig defines two SecurityFilterChain beans. Spring processes them in @Order priority — Chain 1 first, Chain 2 if Chain 1 doesn't match.

Chain 1 — OAuth2 protocol endpoints (@Order(1))

Handles all Spring Authorization Server protocol routes:

EndpointPurpose
GET /oauth2/authorizeAuthorization request — PKCE code challenge, consent
POST /oauth2/tokenToken exchange — code → access_token + SMART extras
GET /oauth2/jwksPublic JWKS for JWT signature verification
POST /oauth2/revokeToken revocation
POST /oauth2/introspectToken introspection
GET /userinfoOIDC user info endpoint
@Bean @Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http) throws Exception {

OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults())
.tokenEndpoint(token -> token
.accessTokenResponseHandler(tokenResponseConverter) // SMART extras
);

http
.exceptionHandling(ex -> ex
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));

return http.build();
}

The accessTokenResponseHandler is the critical wiring that makes SmartTokenResponseConverter fire on every successful token exchange, promoting patient, encounter, and need_patient_banner from JWT claims to top-level JSON fields.

The .oauth2ResourceServer(rs -> rs.jwt(...)) enables the /userinfo endpoint — it accepts the access token as a Bearer credential to return OIDC user info.

Chain 2 — Portal, login, and everything else (@Order(2))

Handles the human-facing routes:

@Bean @Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/.well-known/smart-configuration",
"/oauth2/jwks",
"/actuator/health",
"/login",
"/error"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/portal", true)
);
return http.build();
}

Public routes (no auth required)

RouteWhy public
/.well-known/smart-configurationSMART clients call this before any auth
/oauth2/jwksClients need this to verify JWT signatures
/actuator/healthLoad balancer health checks
/loginLogin form must be accessible without auth
/errorError pages should always be reachable

Protected routes (require login)

RouteHandler
GET /portalLaunchPortalController.portal() — patient picker
POST /portal/launchLaunchPortalController.launch() — creates launch token

Why two chains

Spring Authorization Server needs the OAuth2 endpoints to be protected differently from web pages. The protocol endpoints use HTTP Basic or client auth (for POST /oauth2/token) and must return JSON 401 errors. The portal uses form login and must return HTML 302 redirects to /login.

Splitting them into two filter chains with explicit @Order avoids any ambiguity about which security rules apply to which route.


Package Structure · Architecture Home