Spring security : Why adding the JWT filter before UsernamePasswordAuthenticationFilter in configuration?
Asked Answered
B

2

12

I'm working with Spring security and jwt ,but there is something that i don't understand in the configuration file (same configuration in all tutorials on JWT ) it is why to add the Custom jwt filter before the UsernamePasswordAuthenticationFilter since i already have a public authentication controller based on username and password in somewhere of the project and why not adding it in some other order ?

@Bean
    public JwtAuthTokenFilter authenticationJwtTokenFilter() {
        return new JwtAuthTokenFilter();
    }
@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors().and().csrf().disable()
        .authorizeRequests()
        .antMatchers("/api/auth/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .exceptionHandling().authenticationEntryPoint((AuthenticationEntryPoint) unauthorizedHandler)
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore((Filter) authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

    }
}
Bashful answered 12/12, 2019 at 10:2 Comment(0)
G
18

TL;DR;

I believe it's just a random filter someone on the internet happened to have chosen and shared his/her code somewhere, and it's been adopted that way ever since.


For the curious

I personally went through the Spring Security source code and found out that the filters in Spring application context with their respective @Orders are as below:

org.springframework.security.web.access.channel.ChannelProcessingFilter -> 100
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter -> 300
org.springframework.security.web.context.SecurityContextPersistenceFilter -> 400
org.springframework.security.web.header.HeaderWriterFilter -> 500
org.springframework.web.filter.CorsFilter -> 600
org.springframework.security.web.csrf.CsrfFilter -> 700
org.springframework.security.web.authentication.logout.LogoutFilter -> 800
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter -> 900
org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter -> 1000
org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter -> 1100
org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter -> 1200
org.springframework.security.cas.web.CasAuthenticationFilter -> 1300
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter -> 1400
org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter -> 1500
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -> 1600
org.springframework.security.openid.OpenIDAuthenticationFilter -> 1800
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter -> 1900
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter -> 2000
org.springframework.security.web.session.ConcurrentSessionFilter -> 2100
org.springframework.security.web.authentication.www.DigestAuthenticationFilter -> 2200
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter -> 2300
org.springframework.security.web.authentication.www.BasicAuthenticationFilter -> 2400
org.springframework.security.web.savedrequest.RequestCacheAwareFilter -> 2500
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter -> 2600
org.springframework.security.web.jaasapi.JaasApiIntegrationFilter -> 2700
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -> 2800
org.springframework.security.web.authentication.AnonymousAuthenticationFilter -> 2900
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter -> 3000
org.springframework.security.web.session.SessionManagementFilter -> 3100
org.springframework.security.web.access.ExceptionTranslationFilter -> 3200
org.springframework.security.web.access.intercept.FilterSecurityInterceptor -> 3300
org.springframework.security.web.authentication.switchuser.SwitchUserFilter -> 3400

Please note that the less the order value, the more prioritized the filter.

But these are all the filters that Spring registers. Spring Security on the other hand, being a single filter chain containing a number of filters inside, includes only some of these, which are (in priority order):

Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CorsFilter
  LogoutFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
] 

You can get this list by setting the debug flag to true in the Spring Security config by

@EnableWebSecurity(debug = true)

Where to put the custom Jwt Filter ?

I'd personally place the custom Jwt Filter right after ExceptionTranslationFilter and before FilterSecurityInterceptor, forming below chain:

  Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CorsFilter
  LogoutFilter
                            <------ Here's where the UsernamePasswordAuthenticationFilter 
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  JwtAuthTokenFilter        <------   Here's your filter.
  FilterSecurityInterceptor
] 

You see the motivation in doing so, is to let Spring security exception handler resides in ExceptionTranslatorFilter to handle your exception as is. Because if you place your filter where the UsernamePasswordAuthenticationFilter would be, any possible exception thrown by your filter will not be seen by the ExceptionTranslatorFilter, instead it will go all the way back to the throwable() method of Tomcat's org.apache.catalina.core.StandardHostValve, (if you're using Tomcat as the servlet impl., obviously), and what it is going to do is, redirect your request to the default error path, i.e. /error.

Here's where the fun begins; upon an exception thrown by your filter that is supposed to intercept whatever request coming into your application (say /hello), now a subsequent request to the predefined error path (let's assume /error) will be made. And by luck, if you haven't happened to "permit" that path with your HttpSecurity config, what's next is below series of events:

  1. The same security chain will be applied to the new request with requestURI /error
  2. If your custom filter extends OncePerRequestFilter, which I assume it would, it will NOT BE FILTERED AGAIN, since it would have been marked as FILTERED in the previous pass.
  3. The rest of the filter chain will be invoked in the same order above all the way to the last filter, a.k.a FilterSecurityInterceptor
  4. It'll evaluate the authentication token, pull in the AccessDecisionVoters, which will promptly vote the unfamiliar /error request as unauthenticated, throw an AccessDeniedException and call it a day.
  5. The very next filter on the way back to the client, which is no other than ExceptionTranslationFilter will see this AccessDeniedException, will evaluate it as a regular access denial and call the AuthenticationEntryPoint.commence with the AccessDeniedException being the 3rd parameter.
  6. From then I would assume your code will call sendError within that method, and your client will receive this as a response.

So what's wrong with that, I blocked the request in my filter and the client got the error response ?

What's wrong is, the message client got, was not generated for your request or your error, it was generated for a request to the /error path and an error generated by the FilterSecurityInterceptor.


So what should you do ?

I would humbly suggest that, put your JwtAuthTokenFilter between the last two filters in the security filter chain, and the error you might throw will be handled by default spring security error resolver and you will save the server a roundtrip between the StandardHostValve and the FilterSecurityInterceptor.

http.addFilterAfter(authenticationJwtTokenFilter(), ExceptionTranslationFilter.class);

Or don't and the client will get an error eventually.

Gaspar answered 27/1, 2022 at 20:59 Comment(12)
this is one of the most understandable explanation of Security I have read.Phonon
I would like to mark this as my first bookmark answer in SOF. There are too many copiers and they don't know exactly what is happen, especially it related to Security section. 1 vote, gj @GasparLithe
Thanks to you both guys, I am glad it was helpful to someone :)Gaspar
excellent explanation!!!!! TOPRectrix
So why would spring security put UsernamePasswordAuthenticationFilter at where it is now instead of after ExceptionTranslationFilter? Also, it seems like ExceptionTranslationFilter only handles AccessDeniedException, which is thrown when accessing SecurityContext, so unless we manually throw AccessDeniedException (which we shouldn't), I don't ExceptionTranslationFilter handles the exception for us.Tripedal
Well, I wouldn't know the exact motivation behind it, but it makes sense to me in following aspect:. UsernamePasswordAuthFilter is concerned with the authentication of a request, not authorization. On the other hand, Spring security aims to provide both with a framework with well-defined boundaries that allows separation of concerns more easily.Gaspar
For your second question, ExceptionTranslationFilter handles spring security related exceptions in two categories, AuthenticationException and AccessDeniedException, which are thrown in cases where an Authentication object is invalid or does not contain the authorities required to access a resource, respectively. Finally, you are correct to conclude that if you do not throw AccessDeniedExc the filter wont handle your exception, but that's already the expected behavior. The internal method of this filter 'handleSpringSecurityException' should not handle any other type of exceptions anyways.Gaspar
But hey, this is my interpretation of things under the hood. You can understand things by reverse-engineering only so muchGaspar
Your reasoning of spring security wanting to have a boundary between authentication and authorization makes sense. Though in that case, I feel that we should probably respect it by not crossing the boundary, which would happen if we put our jwt filter after ExceptionTranslationFilter. For the AuthException and AccessDeniedException, I don't think it's the job of a jwt filter to throw either of them so there would be nothing for ExceptionTranslationFilter to catch.Tripedal
I'm not an expert on spring security either so I'm not asserting there is a right/wrong. I'm just trying to follow through your logic and pointing out the parts that don't make sense to me, which are 1) supporting putting UsernamePasswordAuthFilter before ExceptionTranslationFilter but jwt filter after despite them both being an authentication filter, and 2) advertising using ExceptionTranslationFilter to catch the exceptions that a jwt filter does not and should not throw.Tripedal
Your concerns do make sense that both filters might be doing the same task, however as can clearly be seen by the path request takes when the auth filter is placed before the ExceptionTranslationFilter, it surely feels like this is not how things should go. If there is an error with a request it should be handled right then and there, not after bouncing back and forth between the app server-internal components due to a "fortunately" absent "/error" handlerGaspar
That was my motivation in digging this deep and concluding what I shared as answer. I appreciate your constructive criticism.Gaspar
T
0

I believe it is so that we use the authentication result from the username and password instead of the JWT when the user passes both in the same request.

Since both UsernamePasswordAuthenticationFilter and a JWT filter write to SecurityContext, when we authenticate a user with

SecurityContextHolder.getContext().setAuthentication(authentication);

it overwrites the currently authenticated principal.

Putting our JWT filter before UsernamePasswordAuthenticationFilter ensures that if a user provides both the JWT and their username and password in a request, we use the username and password instead of the JWT.

I believe this is a good practice because of the time-based nature of JWT. A JWT can hold oudated information if some user data changes after we issued a JWT and before that JWT expires, whereas the username and password are checked against the database every time so it should always be up to date.


Regarding alegria's very detailed answer, I initially believed in it too, but dug into what ExceptionTranslationFilter does and noticed that it handles two exceptions:

  1. AccessDeniedException, which is thrown by AuthorizationFilter on failed authorizations.
  2. AuthenticationException, which is "[an] abstract superclass for all exceptions related to an Authentication object being invalid for whatever reason".

I don't think a JWT filter should throw either of these. Throwing AccessDeniedException is the job of AuthorizationFilter, and AuthenticationException seems to be for when the authentication token is invalid. A JWT filter is the one making that authentication token, so it seems counterintuitive that we would throw an exception invalidating the token we constructed. That should be done in the classes that read, not write, the Authentication object.

This means that there is no need to put the JWT filter after ExceptionTranslationFilter. The latter does not handle any exception the JWT filter throws.

Tripedal answered 6/9 at 6:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.