Reputation: 131
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);
}
}
Upvotes: 12
Views: 6103
Reputation: 364
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 the 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:
AccessDeniedException
, which is thrown by AuthorizationFilter
on failed authorizations.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.
Upvotes: 0
Reputation: 1145
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 @Order
s 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:
/error
OncePerRequestFilter
, which I assume it would, it will NOT BE FILTERED AGAIN, since it would have been marked as FILTERED in the previous pass.FilterSecurityInterceptor
AccessDecisionVoter
s, which will promptly vote the unfamiliar /error
request as unauthenticated, throw an AccessDeniedException
and call it a day.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.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.
Upvotes: 19