Reputation: 91
I am experimenting with spring security and came across a strange behavior. My idea is to create a security filter that authenticates requests based on JWT (or JWS) tokens:
public class JWTokenFilter extends AbstractAuthenticationProcessingFilter {
public JWTokenFilter(AuthenticationManager authenticationManager) {
super("/**"); //doesn't have any effect, every request still gets considered by this filter
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
throw new TokenException("Token is empty");
}
var authentication = determineAuthentication(token.replace("Bearer","").trim());
//the AbstractAuthenticationProcessingFilter fills the Security context
return this.getAuthenticationManager().authenticate(authentication);
}
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
System.out.println("Asked for "+request.getRequestURI());
return request.getHeader("Authorization") != null;
}
private TokenAuthentication<UserInfo> determineAuthentication(String token) {
var split = token.split("\\.");
if (split.length < 2 || split.length > 3) {
throw new TokenException("Token malformed");
}
if (split.length == 2){
return new JWTAuthentication<>(token);
}else {
return new JWSAuthentication<>(token);
}
}
}
I have 3 @RestController
classes which have their paths mapped:
@RequestMapping("/admin")
@RequestMapping("/all")
@RequestMapping("/anon")
Along with this, I have the following security configuration:
@Configuration
@Order(98)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/all/**","/anon/**")
.and()
.authorizeRequests().antMatchers("/all/**").permitAll()
.and()
.authorizeRequests().antMatchers("/anon/**").anonymous();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().mvcMatchers("/webjars/**", "/css/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Configuration
@Order(99)
public static class TokenSecurityConfig extends WebSecurityConfigurerAdapter{
@Lazy
@Autowired
private JWTokenFilter tokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //having /admin/** or /** makes no difference
.anyRequest().authenticated()
.and().addFilterBefore(tokenFilter,ExceptionTranslationFilter.class);//put this filter near the end of the chain
}
@Bean
public JWTokenFilter tokenFilter(JWTokenAuthenticationProvider jwTokenAuthenticationProvider,JWSTokenAuthenticationProvider jwsTokenAuthenticationProvider){
var list = new ArrayList<AuthenticationProvider>();
list.add(jwsTokenAuthenticationProvider);
list.add(jwTokenAuthenticationProvider);
ProviderManager manager = new ProviderManager(list);
return new JWTokenFilter(manager);
}
}
}
From this configuration here we can see that there are 2 SecurityFilterChan
s (not counting the /webjars
and /css
ones):
"/all/**"
and "/anon/**"
REST routesSince the 1. chain has lower @Order(98)
, than 2. @Order(99)
, that means that the 1. chain will be considered first which is shown by the debugger,
and matched if the incoming request looks like:
curl --request GET \
--url http://localhost:8080/all/hello \
Now what I am experiencing is that the JWTokenFilter
method boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response)
is always called no matter the request path !
And in the console output, I can find Asked for /all/hello
.
Edit:
My spring boot version is 2.3.6.RELEASE
My question is:
Why is the JWTokenFIlter
even asked if it should authenticate requests with paths that are not matched by the SecurityFilterChain
it is a part of?
Upvotes: 0
Views: 1192
Reputation: 21720
I believe I have a better answer, but I wanted to answer your original question as well. I split this into two sections.
Improved Answer
I realize this doesn't answer the original question, but I think you may be better off using the built in support for JWT based authentication. I'd check out the OAuth 2.0 Resource Server section of the reference documentation.
Answer to Original Question
Spring Boot will automatically register any Filter
exposed as a @Bean
for every request directly with the Servlet Container.
You have two options that I see. The first is to avoid exposing the JwtTokenFilter
as a @Bean
.
@Configuration
@Order(99)
public static class TokenSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
JWTokenAuthenticationProvider jwTokenAuthenticationProvider;
@Autowired
JWSTokenAuthenticationProvider jwsTokenAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //having /admin/** or /** makes no difference
.anyRequest().authenticated()
.and().addFilterBefore(tokenFilter(),ExceptionTranslationFilter.class);//put this filter near the end of the chain
}
public JWTokenFilter tokenFilter(){
var list = new ArrayList<AuthenticationProvider>();
list.add(jwsTokenAuthenticationProvider);
list.add(jwTokenAuthenticationProvider);
ProviderManager manager = new ProviderManager(list);
return new JWTokenFilter(manager);
}
}
Alternatively, you can continue exposing JwtTokenFilter
as a @Bean
and create a FilterRegistrationBean
that disables registration.
@Bean
public FilterRegistrationBean registration(JWTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
Upvotes: 2