Hello all support is appreciated, I set up Spring Core (5.3.39) with Spring Secuirty (5.8.16) to use Oauth2 Resource Server and Authorization Server is Keycloak. I have situation ordering security filter chains and using SecurityMatchers did not activate BearerTokenAuthenticationFilter of second chain. How is it done right?
SpringSecurityConfig.java:
package edu.remad.tutoring2.security.config;
import java.util.HashMap;
import java.util.Map;
import .springframework.beans.factory.annotation.Value;
import .springframework.context.annotation.Bean;
import .springframework.context.annotation.Configuration;
import .springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import .springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import .springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import .springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import .springframework.security.crypto.password.DelegatingPasswordEncoder;
import .springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
public class SpringSecurityConfig {
@Value("${spring.websecurity.debug:true}")
boolean webSecurityDebug;
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.debug(webSecurityDebug);
}
@Bean
PasswordEncoder passwordEncoder() {
String idForEncode = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
}
SecurityFilterChainsConfig.java:
package edu.remad.tutoring2.security.config;
import .springframework.beans.factory.annotation.Autowired;
import .springframework.context.annotation.Bean;
import .springframework.context.annotation.Configuration;
import .springframework.core.annotation.Order;
import .springframework.security.config.annotation.web.builders.HttpSecurity;
import .springframework.security.config.http.SessionCreationPolicy;
import .springframework.security.web.SecurityFilterChain;
import .springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import .springframework.security.web.authentication.www.BasicAuthenticationFilter;
import .springframework.security.web.csrf.CookieCsrfTokenRepository;
import .springframework.security.web.header.HeaderWriterFilter;
import .springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import .springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;
import .springframework.security.web.util.matcher.AntPathRequestMatcher;
import edu.remad.tutoring2.jwt.Tutoring2CustomJwtAuthenticationConverter;
import edu.remad.tutoring2.security.ContentSecurityPolicySettings;
import edu.remad.tutoring2.security.filters.DebugLoggingFilter;
import edu.remad.tutoring2.security.filters.HttpHeadersFilter;
import edu.remad.tutoring2.security.filters.TenantFilter;
@Configuration
public class SecurityFilterChainsConfig {
private static final ClearSiteDataHeaderWriter.Directive[] COOKIES = Directive.values();
@Autowired
private ContentSecurityPolicySettings contentSecurityPolicies;
@Autowired
private Tutoring2CustomJwtAuthenticationConverter jwtAuthConverter;
/**
* Does form login filter chain and has also http security.
*
* @param http similar to spring security xml config for filtering request
* @return created security filter chain, {@link SecurityFilterChain}
* @throws Exception
*/
@Bean
@Order(1)
SecurityFilterChain formloginSecurityFilterChain(HttpSecurity http) throws Exception {
http.cors().and().headers(headers -> headers.xssProtection().and()
.contentSecurityPolicy(contentSecurityPolicies.getContentSecurityPolicies()));
http.addFilterAfter(new TenantFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(new HttpHeadersFilter(), HeaderWriterFilter.class)
.addFilterAfter(new DebugLoggingFilter(), HttpHeadersFilter.class)
.securityContext((securityContext) -> securityContext.requireExplicitSave(true))
.sessionManagement(
session -> session.maximumSessions(1).maxSessionsPreventsLogin(true).expiredUrl("/login"))
.authorizeRequests(requests -> requests.antMatchers("/", "/helloWorld", "/logoutSuccess", "/signup", "/api/v1/csrf")
.permitAll().antMatchers("/hello", "/bye", "/login", "/logout", "/templates/**").authenticated())
.formLogin(login -> login.loginPage("/myCustomLogin").loginProcessingUrl("/process-login")
.defaultSuccessUrl("/hello", true)).csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/logoutSuccess")
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES))));
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain oauth2rescourceserverSecurityFilterChain(HttpSecurity http) throws Exception {
return http.securityMatcher(AntPathRequestMatcher.antMatcher("/v2/**"))
.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()).csrf(csrf -> csrf.disable())
.oauth2ResourceServer(server -> server.jwt().jwtAuthenticationConverter(jwtAuthConverter))
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).build();
}
}
Oauth2ResourcServerConfig.java:
package edu.remad.tutoring2.security.config;
import java.MalformedURLException;
import java.URL;
import .springframework.context.annotation.Bean;
import .springframework.context.annotation.Configuration;
import .springframework.security.oauth2.jwt.JwtDecoder;
import .springframework.security.oauth2.jwt.NimbusJwtDecoder;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
@Configuration
public class Oauth2ResourcServerConfig {
private String keySetUri = "http://192.168.120.59:8080/realms/ConnectTrial/protocol/openid-connect/certs";
@Bean
JwtDecoder jwtDecoder() throws KeySourceException, MalformedURLException {
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(keySetUri));
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
}
Tutoring2CustomJwtAuthenticationConverter.java:
package edu.remad.tutoring2.jwt;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CLAIM_RESSOURCE_ACCESS;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CONVERTER_PRINCIPAL_ATTRIBUTE;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CONVERTER_RESOURCE_ID;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_ROLES_KEY;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import .springframework.core.convert.converter.Converter;
import .springframework.lang.NonNull;
import .springframework.security.authentication.AbstractAuthenticationToken;
import .springframework.security.core.GrantedAuthority;
import .springframework.security.core.authority.SimpleGrantedAuthority;
import .springframework.security.oauth2.jwt.Jwt;
import .springframework.security.oauth2.jwt.JwtClaimNames;
import .springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import .springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import .springframework.stereotype.Component;
/**
* Converts roles from Keycloak to Spring Security roles. It reads JWT and fetches all claims and roles as roles.
*/
@Component
public class Tutoring2CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
/**
* Default Constructor
*/
public Tutoring2CustomJwtAuthenticationConverter() {
jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
}
@Override
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream
.concat(jwtGrantedAuthoritiesConverter.convert(jwt).stream(), extractJwtResourceRoles(jwt).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private Collection<? extends GrantedAuthority> extractJwtResourceRoles(Jwt jwt) {
if (jwt.getClaimAsMap(JWT_CLAIM_RESSOURCE_ACCESS) == null) {
return Set.of();
}
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
if (resourceAccess.get(JWT_CONVERTER_RESOURCE_ID) == null) {
return Set.of();
}
if (resourceAccess.get(JWT_CONVERTER_RESOURCE_ID) == null) {
return Set.of();
}
Map<String, Object> resource = (Map<String, Object>) resourceAccess.get(JWT_CONVERTER_RESOURCE_ID);
Collection<String> resourceRoles = (Collection<String>) resource.get(JWT_ROLES_KEY);
return resourceRoles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (JWT_CONVERTER_PRINCIPAL_ATTRIBUTE != null) {
claimName = JWT_CONVERTER_PRINCIPAL_ATTRIBUTE;
}
return jwt.getClaim(claimName);
}
}
pom.xml:
<project xmlns=".0.0"
xmlns:xsi=";
xsi:schemaLocation=".0.0 .0.0.xsd">
<!-- ###################### -->
<!-- start of project setup -->
<!-- ###################### -->
<modelVersion>4.0.0</modelVersion>
<groupId>edu.remad</groupId>
<artifactId>tutoring2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>Tutoring 2 Maven Webapp</name>
<url>;/url>
<!-- #################### -->
<!-- end of project setup -->
<!-- #################### -->
<!-- ################### -->
<!-- start of properties -->
<!-- ################### -->
<properties>
<mavenpiler.target>11</mavenpiler.target>
<mavenpiler.source>11</mavenpiler.source>
<encoding>UTF-8</encoding>
<spring.version>5.3.39</spring.version>
<spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.14</spring.boot.version>
<junit5.version>5.10.0</junit5.version>
<lombok.version>1.18.30</lombok.version>
<spring.oauth2.resourceserver.version>2.7.14</spring.oauth2.resourceserver.version>
<version>3.3.2</version>
</properties>
<!-- ################# -->
<!-- end of properties -->
<!-- ################# -->
<!-- ##################### -->
<!-- start of dependencies -->
<!-- ##################### -->
<dependencies>
<!-- .springframework/spring-beans -->
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- spring security needed deoendencies -->
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>5.8.16</version>
</dependency>
<!-- Spring framework -->
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.6.9.Final</version>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.3.Final</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- CGLib for @Configuration -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2.2</version>
<scope>runtime</scope>
</dependency>
<!-- Servlet Spec -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!--<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Maven Plugins section -->
<dependency>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</dependency>
<!-- JAXB, for version 3 use Jakarta -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.8</version>
</dependency>
<dependency>
<groupId>.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- resource-server -->
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<!-- start of own implementations -->
<dependency>
<groupId>edu.remad</groupId>
<artifactId>ical4j-builder</artifactId>
<version>1.0.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>edu.remad</groupId>
<artifactId>pdf-toolboxing</artifactId>
<version>0.30.1-SNAPSHOT</version>
</dependency>
<!-- end of own implementation -->
</dependencies>
<!-- ################### -->
<!-- end of dependencies -->
<!-- ################### -->
<!-- ################# -->
<!-- start of profiles -->
<!-- ################# -->
<profiles>
<profile>
<id>development</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
</profile>
<profile>
<id>withoutTests</id>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
</profile>
<profile>
<id>withIntegrationTests</id>
<properties>
</properties>
</profile>
</profiles>
<!-- ############### -->
<!-- end of profiles -->
<!-- ############### -->
<!-- ###################### -->
<!-- start of build section -->
<!-- ###################### -->
<build>
<finalName>tutoring2</finalName>
<plugins>
<plugin>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${mavenpiler.target}</source>
<target>${mavenpiler.source}</target>
<annotationProcessorPaths>
<path>
<groupId>.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.1</version>
<dependencies>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit5.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<!-- #################### -->
<!-- end of build section -->
<!-- #################### -->
</project>
Hello all support is appreciated, I set up Spring Core (5.3.39) with Spring Secuirty (5.8.16) to use Oauth2 Resource Server and Authorization Server is Keycloak. I have situation ordering security filter chains and using SecurityMatchers did not activate BearerTokenAuthenticationFilter of second chain. How is it done right?
SpringSecurityConfig.java:
package edu.remad.tutoring2.security.config;
import java.util.HashMap;
import java.util.Map;
import .springframework.beans.factory.annotation.Value;
import .springframework.context.annotation.Bean;
import .springframework.context.annotation.Configuration;
import .springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import .springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import .springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import .springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import .springframework.security.crypto.password.DelegatingPasswordEncoder;
import .springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
public class SpringSecurityConfig {
@Value("${spring.websecurity.debug:true}")
boolean webSecurityDebug;
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.debug(webSecurityDebug);
}
@Bean
PasswordEncoder passwordEncoder() {
String idForEncode = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
}
SecurityFilterChainsConfig.java:
package edu.remad.tutoring2.security.config;
import .springframework.beans.factory.annotation.Autowired;
import .springframework.context.annotation.Bean;
import .springframework.context.annotation.Configuration;
import .springframework.core.annotation.Order;
import .springframework.security.config.annotation.web.builders.HttpSecurity;
import .springframework.security.config.http.SessionCreationPolicy;
import .springframework.security.web.SecurityFilterChain;
import .springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import .springframework.security.web.authentication.www.BasicAuthenticationFilter;
import .springframework.security.web.csrf.CookieCsrfTokenRepository;
import .springframework.security.web.header.HeaderWriterFilter;
import .springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import .springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;
import .springframework.security.web.util.matcher.AntPathRequestMatcher;
import edu.remad.tutoring2.jwt.Tutoring2CustomJwtAuthenticationConverter;
import edu.remad.tutoring2.security.ContentSecurityPolicySettings;
import edu.remad.tutoring2.security.filters.DebugLoggingFilter;
import edu.remad.tutoring2.security.filters.HttpHeadersFilter;
import edu.remad.tutoring2.security.filters.TenantFilter;
@Configuration
public class SecurityFilterChainsConfig {
private static final ClearSiteDataHeaderWriter.Directive[] COOKIES = Directive.values();
@Autowired
private ContentSecurityPolicySettings contentSecurityPolicies;
@Autowired
private Tutoring2CustomJwtAuthenticationConverter jwtAuthConverter;
/**
* Does form login filter chain and has also http security.
*
* @param http similar to spring security xml config for filtering request
* @return created security filter chain, {@link SecurityFilterChain}
* @throws Exception
*/
@Bean
@Order(1)
SecurityFilterChain formloginSecurityFilterChain(HttpSecurity http) throws Exception {
http.cors().and().headers(headers -> headers.xssProtection().and()
.contentSecurityPolicy(contentSecurityPolicies.getContentSecurityPolicies()));
http.addFilterAfter(new TenantFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(new HttpHeadersFilter(), HeaderWriterFilter.class)
.addFilterAfter(new DebugLoggingFilter(), HttpHeadersFilter.class)
.securityContext((securityContext) -> securityContext.requireExplicitSave(true))
.sessionManagement(
session -> session.maximumSessions(1).maxSessionsPreventsLogin(true).expiredUrl("/login"))
.authorizeRequests(requests -> requests.antMatchers("/", "/helloWorld", "/logoutSuccess", "/signup", "/api/v1/csrf")
.permitAll().antMatchers("/hello", "/bye", "/login", "/logout", "/templates/**").authenticated())
.formLogin(login -> login.loginPage("/myCustomLogin").loginProcessingUrl("/process-login")
.defaultSuccessUrl("/hello", true)).csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/logoutSuccess")
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES))));
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain oauth2rescourceserverSecurityFilterChain(HttpSecurity http) throws Exception {
return http.securityMatcher(AntPathRequestMatcher.antMatcher("/v2/**"))
.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()).csrf(csrf -> csrf.disable())
.oauth2ResourceServer(server -> server.jwt().jwtAuthenticationConverter(jwtAuthConverter))
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).build();
}
}
Oauth2ResourcServerConfig.java:
package edu.remad.tutoring2.security.config;
import java.MalformedURLException;
import java.URL;
import .springframework.context.annotation.Bean;
import .springframework.context.annotation.Configuration;
import .springframework.security.oauth2.jwt.JwtDecoder;
import .springframework.security.oauth2.jwt.NimbusJwtDecoder;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
@Configuration
public class Oauth2ResourcServerConfig {
private String keySetUri = "http://192.168.120.59:8080/realms/ConnectTrial/protocol/openid-connect/certs";
@Bean
JwtDecoder jwtDecoder() throws KeySourceException, MalformedURLException {
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(keySetUri));
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
}
Tutoring2CustomJwtAuthenticationConverter.java:
package edu.remad.tutoring2.jwt;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CLAIM_RESSOURCE_ACCESS;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CONVERTER_PRINCIPAL_ATTRIBUTE;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CONVERTER_RESOURCE_ID;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_ROLES_KEY;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import .springframework.core.convert.converter.Converter;
import .springframework.lang.NonNull;
import .springframework.security.authentication.AbstractAuthenticationToken;
import .springframework.security.core.GrantedAuthority;
import .springframework.security.core.authority.SimpleGrantedAuthority;
import .springframework.security.oauth2.jwt.Jwt;
import .springframework.security.oauth2.jwt.JwtClaimNames;
import .springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import .springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import .springframework.stereotype.Component;
/**
* Converts roles from Keycloak to Spring Security roles. It reads JWT and fetches all claims and roles as roles.
*/
@Component
public class Tutoring2CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
/**
* Default Constructor
*/
public Tutoring2CustomJwtAuthenticationConverter() {
jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
}
@Override
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream
.concat(jwtGrantedAuthoritiesConverter.convert(jwt).stream(), extractJwtResourceRoles(jwt).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private Collection<? extends GrantedAuthority> extractJwtResourceRoles(Jwt jwt) {
if (jwt.getClaimAsMap(JWT_CLAIM_RESSOURCE_ACCESS) == null) {
return Set.of();
}
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
if (resourceAccess.get(JWT_CONVERTER_RESOURCE_ID) == null) {
return Set.of();
}
if (resourceAccess.get(JWT_CONVERTER_RESOURCE_ID) == null) {
return Set.of();
}
Map<String, Object> resource = (Map<String, Object>) resourceAccess.get(JWT_CONVERTER_RESOURCE_ID);
Collection<String> resourceRoles = (Collection<String>) resource.get(JWT_ROLES_KEY);
return resourceRoles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (JWT_CONVERTER_PRINCIPAL_ATTRIBUTE != null) {
claimName = JWT_CONVERTER_PRINCIPAL_ATTRIBUTE;
}
return jwt.getClaim(claimName);
}
}
pom.xml:
<project xmlns="http://maven.apache./POM/4.0.0"
xmlns:xsi="http://www.w3./2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache./POM/4.0.0 https://maven.apache./xsd/maven-4.0.0.xsd">
<!-- ###################### -->
<!-- start of project setup -->
<!-- ###################### -->
<modelVersion>4.0.0</modelVersion>
<groupId>edu.remad</groupId>
<artifactId>tutoring2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>Tutoring 2 Maven Webapp</name>
<url>http://maven.apache.</url>
<!-- #################### -->
<!-- end of project setup -->
<!-- #################### -->
<!-- ################### -->
<!-- start of properties -->
<!-- ################### -->
<properties>
<mavenpiler.target>11</mavenpiler.target>
<mavenpiler.source>11</mavenpiler.source>
<encoding>UTF-8</encoding>
<spring.version>5.3.39</spring.version>
<spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.14</spring.boot.version>
<junit5.version>5.10.0</junit5.version>
<lombok.version>1.18.30</lombok.version>
<spring.oauth2.resourceserver.version>2.7.14</spring.oauth2.resourceserver.version>
<version>3.3.2</version>
</properties>
<!-- ################# -->
<!-- end of properties -->
<!-- ################# -->
<!-- ##################### -->
<!-- start of dependencies -->
<!-- ##################### -->
<dependencies>
<!-- https://mvnrepository/artifact/.springframework/spring-beans -->
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- spring security needed deoendencies -->
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>5.8.16</version>
</dependency>
<!-- Spring framework -->
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.6.9.Final</version>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.3.Final</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- CGLib for @Configuration -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2.2</version>
<scope>runtime</scope>
</dependency>
<!-- Servlet Spec -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!--<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Maven Plugins section -->
<dependency>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</dependency>
<!-- JAXB, for version 3 use Jakarta -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.8</version>
</dependency>
<dependency>
<groupId>.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- resource-server -->
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<dependency>
<groupId>.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<!-- start of own implementations -->
<dependency>
<groupId>edu.remad</groupId>
<artifactId>ical4j-builder</artifactId>
<version>1.0.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>edu.remad</groupId>
<artifactId>pdf-toolboxing</artifactId>
<version>0.30.1-SNAPSHOT</version>
</dependency>
<!-- end of own implementation -->
</dependencies>
<!-- ################### -->
<!-- end of dependencies -->
<!-- ################### -->
<!-- ################# -->
<!-- start of profiles -->
<!-- ################# -->
<profiles>
<profile>
<id>development</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
</profile>
<profile>
<id>withoutTests</id>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
</profile>
<profile>
<id>withIntegrationTests</id>
<properties>
</properties>
</profile>
</profiles>
<!-- ############### -->
<!-- end of profiles -->
<!-- ############### -->
<!-- ###################### -->
<!-- start of build section -->
<!-- ###################### -->
<build>
<finalName>tutoring2</finalName>
<plugins>
<plugin>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${mavenpiler.target}</source>
<target>${mavenpiler.source}</target>
<annotationProcessorPaths>
<path>
<groupId>.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.1</version>
<dependencies>
<dependency>
<groupId>.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit5.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<!-- #################### -->
<!-- end of build section -->
<!-- #################### -->
</project>
Share
Improve this question
edited Mar 18 at 21:24
dur
17k26 gold badges90 silver badges144 bronze badges
asked Mar 16 at 19:06
ReMadWebReMadWeb
681 silver badge6 bronze badges
1 Answer
Reset to default 1The filter chain with @Order(1)
must have a securityMatcher
for a filter chain with @Order(2)
to have a chance to be tried. So, switch the @Order
definitions or move securityMatcher
.
The security filter chains are tried in @Order
(1 is tried before 2). The first matching a request is used and others are discarded. If there is no securityMatcher
, a filter chain matches all requests, which is a good choice for the last filter chain in order (acts as default), but not for the 1st (as in your conf).
As a side note, you could find this starter of mine useful. It would save you the burden of maintaining an authorities converter and an oauth2ResourceServer
filter chain. But be aware that the resource server filter chain auto-configured by my starter has no securityMatcher
and lowest precedence. So in addition to removing your oauth2rescourceserverSecurityFilterChain
, Oauth2ResourcServerConfig
, and Tutoring2CustomJwtAuthenticationConverter
, you'll have to define a securityMatcher
for your formloginSecurityFilterChain
(with something similar to what you have to authorizeRequests
)