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:
| Endpoint | Purpose |
|---|---|
GET /oauth2/authorize | Authorization request — PKCE code challenge, consent |
POST /oauth2/token | Token exchange — code → access_token + SMART extras |
GET /oauth2/jwks | Public JWKS for JWT signature verification |
POST /oauth2/revoke | Token revocation |
POST /oauth2/introspect | Token introspection |
GET /userinfo | OIDC 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)
| Route | Why public |
|---|---|
/.well-known/smart-configuration | SMART clients call this before any auth |
/oauth2/jwks | Clients need this to verify JWT signatures |
/actuator/health | Load balancer health checks |
/login | Login form must be accessible without auth |
/error | Error pages should always be reachable |
Protected routes (require login)
| Route | Handler |
|---|---|
GET /portal | LaunchPortalController.portal() — patient picker |
POST /portal/launch | LaunchPortalController.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.