Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
Expand Down Expand Up @@ -118,42 +120,70 @@ public class AuthorizationServerConfig {
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) {
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// Canonical Spring Security 7.x Authorization Server setup.
// The Spring Authorization Server 1.x static
// OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) was
// removed in Spring Security 7.x; the replacement pattern manually
// installs the configurer via http.with(...) and scopes the chain via
// http.securityMatcher(configurer.getEndpointsMatcher()), so THIS chain
// only claims the auth-server endpoints (/oauth2/authorize, /oauth2/token,
// /oauth2/jwks, /oauth2/introspect, /oauth2/revoke, /connect/register,
// /userinfo, etc.). Everything else falls through to
// defaultSecurityFilterChain @Order(2).
//
// Without this scoping, the resource-server bearer-token filter (added
// by .oauth2ResourceServer().jwt(...)) intercepts POST /oauth2/token
// before the token-endpoint filter can run its basic-auth handler.
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer();
authorizationServerConfigurer.oidc(Customizer.withDefaults()); // OIDC 1.0
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/assets/**", "/webjars/**", "/login").permitAll())
.formLogin(Customizer.withDefaults())
.oauth2AuthorizationServer(authorizationServer ->
authorizationServer.oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0
)
.securityMatcher(endpointsMatcher)
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.csrf(Customizer.withDefaults())
// Redirect to the login page when not authenticated from the authorization endpoint
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.with(authorizationServerConfigurer, Customizer.withDefaults())
// Redirect HTML user-agents to the login page when accessing the
// authorization endpoint without an active session
.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration.
// OIDC auto-configures JWT validation for self-protected endpoints
// (id_token signing requires JWT). Outbound tokens to ESPI clients
// remain opaque via accessTokenFormat(REFERENCE) on each RegisteredClient.
// Cannot configure both .jwt() and .opaqueToken() on the same chain
// in Spring Security 7.x.
// Accept access tokens for /userinfo and /connect/register.
// OIDC always issues id_token as JWT, so JWT is the right token
// type here. Outbound tokens to ESPI clients remain opaque via
// accessTokenFormat(REFERENCE) on each RegisteredClient.
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt(Customizer.withDefaults())
);

return http.build();
}

/**
* Default Security Filter Chain for everything NOT claimed by the
* authorization-server endpoints matcher: login form, static resources,
* H2 console (in dev), and any custom controllers.
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
"/assets/**", "/webjars/**",
"/login", "/error",
"/.well-known/**",
"/actuator/health", "/actuator/info"
).permitAll()
.anyRequest().authenticated()
)
// HTTPS Channel Security for Production
//should be able to use property server.ssl.enabled=true
//todo - test this
// .requiresChannel(channel -> {
// if (requireHttps) {
// channel.anyRequest().requiresSecure();
// }
// })
// Enhanced Security Headers for ESPI Compliance
.formLogin(Customizer.withDefaults())
.csrf(Customizer.withDefaults())
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
.contentTypeOptions(Customizer.withDefaults())
Expand All @@ -175,62 +205,6 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
return http.build();
}

/**
* Default Security Filter Chain for non-OAuth2 endpoints
* <p>
* Handles authentication for:
* - Login page
* - User consent page
* - Static resources
*/
//todo remove if not needed
// @Bean
// @Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
// http
//********Moved to order 1
// .authorizeHttpRequests((authorize) -> authorize
// .requestMatchers("/assets/**", "/webjars/**", "/login").permitAll()
// .anyRequest().authenticated()
// )
// Form login handles the redirect to the login page from the
// authorization server filter chain
//******moved to order 1
// .formLogin(Customizer.withDefaults())
// HTTPS Channel Security for Production (Default Security Chain)
//should be able to use property server.ssl.enabled=true
//todo - test this
// .requiresChannel(channel -> {
// if (requireHttps) {
// channel.anyRequest().requiresSecure();
// }
// })
// Enhanced Security Headers
// ****Dup of Order 1
// .headers(headers -> headers
// .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
// .contentTypeOptions(Customizer.withDefaults())
// .httpStrictTransportSecurity(hstsConfig -> hstsConfig
// .maxAgeInSeconds(31536000)
// .includeSubDomains(true)
// .preload(true)
// )
// .referrerPolicy(referrer -> referrer
// .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
// )
// )
// Secure session configuration
// ******Moved to order 1
// .sessionManagement(session -> session
// .sessionCreationPolicy(org.springframework.security.config.http.SessionCreationPolicy.IF_REQUIRED)
// .maximumSessions(1)
// .maxSessionsPreventsLogin(false)
// );

return http.build();
}

/**
* Registered Client Repository
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,20 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.jackson.SecurityJacksonModules;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;

import java.sql.ResultSet;
import java.sql.SQLException;
Expand Down Expand Up @@ -98,7 +102,18 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
public JdbcRegisteredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
this.jdbcTemplate = jdbcTemplate;
this.passwordEncoder = passwordEncoder;
this.objectMapper = new ObjectMapper();
// Register Spring Security's Jackson modules so typed values inside
// ClientSettings / TokenSettings (e.g. OAuth2TokenFormat.REFERENCE)
// round-trip correctly through serialize -> DB -> deserialize. Without
// these modules, typed values come back as LinkedHashMap and crash
// downstream consumers with ClassCastException (see #127 for the
// architectural plan to swap to Spring's stock JdbcRegisteredClientRepository).
ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
List<JacksonModule> securityModules = SecurityJacksonModules.getModules(classLoader);
this.objectMapper = JsonMapper.builder()
.addModules(securityModules)
.addModule(new OAuth2AuthorizationServerJacksonModule())
.build();
}

@Override
Expand Down
Loading