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