I am working on a Spring Boot project with a particular requirement in the Spring Security configuration.
Basically my Spring Boot project exposes some APIs: one of them must be protected using the basic authentication (username and password), the others must be protected by JWT token authentication.
NOTE: why this behavior? I'll explain it briefly. At the moment I have 2 micro-services (then I will have more). The first one (this mentioned project) is intended to handle users on the database. It contains a specific controller method that take the user e-mail address (the username) and return all the user information to the other micro-service that will use these information in order to generate a JWT token. So this method is protected by basic authentication (so the micro-service that create the JWT token and the other that interact with the DB can communicate in a secure way using a "service user credential"). This project (the one on which we are speaking about that interact with the DB) will also contains some other APIs that are used from the clients so these APIs must be protected with a JWT token generated by the second micro-service.
So basically I am trying to split the Spring Security configuration using two configuration classes:
The first configuration class protect my API intended to return the user details to the micro-service that will generate the JWT token. This behavior worked fine (I was able to call an authorization API on the microservice intended to generate the JWT token, this call the API that return the user information on this micro-service that interact with my DB and the token was correctly generated.
The second configuration class is intended to protect all the other APIs that must be protected by a JWT token authentication (this because these are APIs called by the client to perform operation like: add a new user, change a specific user details, delete a user, list all the users, etcetc...so these are operation that can be performed by logged user with different privileges and these privileges are defined into my token).
And here I am finding some difficulties.
This is my SecurityConfiguration configuration class that is intended to configure the basic authentication protecting the single API that take the username and return the user information to the other micro-service that will generate the JWT token:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static String REALM = "REAME";
private static final String[] USER_MATCHER = { "/api/user/email/**"};
private static final String[] ADMIN_MATCHER = { "/api/user/email/**"};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(USER_MATCHER).hasAnyRole("USER")
.antMatchers(ADMIN_MATCHER).hasAnyRole("ADMIN")
.antMatchers("/api/users/test").permitAll()
//.antMatchers("/api/users/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public AuthEntryPoint getBasicAuthEntryPoint()
{
return new AuthEntryPoint();
}
/* To allow Pre-flight [OPTIONS] request from browser */
@Override
public void configure(WebSecurity web)
{
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
@Bean
public BCryptPasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
};
@Bean
@Override
public UserDetailsService userDetailsService()
{
UserBuilder users = User.builder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users
.username("ReadUser")
.password(new BCryptPasswordEncoder().encode("BimBumBam_2018"))
.roles("USER").build());
manager.createUser(users
.username("Admin")
.password(new BCryptPasswordEncoder().encode("MagicaBula_2018"))
.roles("USER", "ADMIN").build());
return manager;
}
}
As you can see this protect with the basic authentication the /api/user/email/** endpoint. This is the endpoint that from the user e-mail (the username) return the user info to the micro-service that will generate the JWT token. It worked fine: my two micro-servics interacted correctly and the JWT token was correctly generated by the other micro-service. This was true untill I insert the second configuration class that was intended to protect all the others API by this JWT token.
This is my new JWTWebSecurityConfig class that is intended to protect all the other APIs of this project that interact with the database tables related to the users of my system:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JWTWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUnAuthorizedResponseAuthenticationEntryPoint jwtUnAuthorizedResponseAuthenticationEntryPoint;
@Autowired
@Qualifier("customUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenAuthorizationOncePerRequestFilter jwtAuthenticationTokenFilter;
@Value("${sicurezza.uri}")
private String authenticationPath;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoderBean());
}
@Bean
public PasswordEncoder passwordEncoderBean()
{
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
private static final String[] NOAUTH_MATCHER = {"/api/articoli/noauth/**"};
private static final String[] USER_MATCHER = { "/api/users/jwttest"};
private static final String[] ADMIN_MATCHER = { "/api/users/jwttest" };
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity.csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtUnAuthorizedResponseAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(NOAUTH_MATCHER).permitAll() //End Point che non richiede autenticazione
.antMatchers(USER_MATCHER).hasAnyRole("USER")
.antMatchers(ADMIN_MATCHER).hasAnyRole("ADMIN")
.anyRequest().authenticated();
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().frameOptions()
.sameOrigin().cacheControl();
}
@Override
public void configure(WebSecurity webSecurity)
{
webSecurity.ignoring()
.antMatchers(HttpMethod.POST, authenticationPath)
.antMatchers(HttpMethod.OPTIONS, "/**")
.and().ignoring()
.antMatchers(HttpMethod.GET, "/");
}
}
As you can see at the moment it is protecting only this test API endpoint that I defined in a controller of this project /api/users/jwttest. So basically what I want is use the generated JWT token to interact with this JWT protected endpoint.
I am not totally sure that my approach is correct. At the moment running this project (after that I inserted this second configuration class) it give me the following error at project startup:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Injection of autowired dependencies failed; nested exception is java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on com.easydefi.users.security.SecurityConfiguration$$EnhancerBySpringCGLIB$$fe033b29@3f142e87, so it cannot be used on com.easydefi.users.security.jwt.JWTWebSecurityConfig$$EnhancerBySpringCGLIB$$d9d962a3@552fffc8 too.
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:405) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.12.jar:5.3.12]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.12.jar:5.3.12]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.5.6.jar:2.5.6]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-2.5.6.jar:2.5.6]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) ~[spring-boot-2.5.6.jar:2.5.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) ~[spring-boot-2.5.6.jar:2.5.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-2.5.6.jar:2.5.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1332) ~[spring-boot-2.5.6.jar:2.5.6]
at com.easydefi.users.GetUserWsApplication.main(GetUserWsApplication.java:10) ~[classes/:na]
Caused by: java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on com.easydefi.users.security.SecurityConfiguration$$EnhancerBySpringCGLIB$$fe033b29@3f142e87, so it cannot be used on com.easydefi.users.security.jwt.JWTWebSecurityConfig$$EnhancerBySpringCGLIB$$d9d962a3@552fffc8 too.
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(WebSecurityConfiguration.java:165) ~[spring-security-config-5.5.3.jar:5.5.3]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:567) ~[na:na]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:724) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) ~[spring-beans-5.3.12.jar:5.3.12]
... 17 common frames omitted
Is it possible to confgure Spring Security to protect some APIs with basic authentication and some other APIs with JWT token authentication? Can this approach be considered correct? What is wrong in my code? What am I missing? How can I try to solve this error?