Spring: Customizing the “weird” UsernamePasswordAuthenticationFilter
230201-18
An implementation for “illuminati”. Use it just once for both: Basic Authentication and JWT Bearer token Authorization.
Preamble
In the last part of my previous post, I presented a similar solution for handling both Basic Authentication and JWT Bearer token Authorization, using only one filter. That solution was based on an implementation of a custom filter that extended the OncePerRequestFilter. Here, I am going to show how you can do this, with another custom implementation, but this time, it will be based on the “weird” UsernamePasswordAuthenticationFilter.
Furthermore, special focus is also given to the error handling, for authentication and authorization failures.
It is obvious that you have to have a good understanding of Spring, Spring Boot, security, and other related subjects. Other posts of mine can add a lot to those subjects. Furthermore, be aware that all code examples provided are base on Spring6 and Spring Boot 3 FilterChainSecurity.
Intro
The UsernamePasswordAuthenticationFilter seems to be one of most widely used filters, especially when it comes to username / password Authentication.
The default behavior of this filter is to a obtain the user credentials (username and password) provided through an HTML login form and attempt to authenticate the user.
For instance, using just the security configuration provided at the official docs here.
@Bean public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception { http .formLogin(withDefaults()); // ... return http.build(); }
You can see that the Spring autoconfigures the default login page for us (http://localhost:8080/login):
Note that you can find the auto-generated password for the default username “user” in the application startup logs:
Of course, since there is no logic behind the scenes, you probably will end up getting the Whitelabel Error Page. However, this is not the point of this post.
Taking a closer look at the application startup logs, you can also notice the set of the default filters in the filterchain, Spring generated automatically, for our .formLogin case:
As you can see above the UsernamePasswordAuthenticationFilter is among the auto-generated filters. However, here we will focus on how we can use the UsernamePasswordAuthenticationFilter with a REST API application and protect our endpoints.
Applying the UsernamePasswordAuthenticationFilter with its default settings
The minimum possible CustomSecurityConfiguration class
What we need is just the default AuthenticationManager (ProviderManager) that Spring auto-configures for us, and which should be passed in the UsernamePasswordAuthenticationFilter. So, the minimum possible custom security configuration is given below:
The Controller with the default handlers
You might be already aware that the UsernamePasswordAuthenticationFilter by default, uses the “/login” endpoint to access the username and password URI parameters passed from a login form with a POST request. After successful authentication, the filter, also by default, redirects to the “/” home endpoint. Finally, an “/error” endpoint handler can be used to handle any error that occurs.
Below, is an example for also a minimum possible controller, that can be used for dealing with the above default settings of the UsernamePasswordAuthenticationFilter. Note that this is a @RestController (and not a Spring MVC @Controller).
Testing the application
Grab the auto-generated password from the application logs and use your Postman to send a POST request to the “/login” endpoint. If the username and password are correct, the filter redirects you and you receive a response sent by the “/” home endpoint (in our case from the ‘myhome’ handler method). The POST request can be sent either via parameters or via a x-www-form-urlencoded Body.
Note: I avoid using the curl here since it is a bit tricky the change from POST to GET method because of the auto-redirection from “/login” to “/” (home) endpoint. (You can change the home HTTP method from @GetMapping to @PostMapping and check that after the change the curl
Use an in-memory UserDetailsService
Copying/using an auto-generated password is not that comfortable. So, a better approach is to use our own in-memory UserDetailsService. The code below provides such an example:
Find the so far full code here.
Setting an Ant Matcher
Many times, the default /login endpoint matcher is not the desired one and it should be changed. The UsernamePasswordAuthenticationFilter offers us 2 methods that can be used for setting up the authentication endpoint and/or the AntMatcher. Here, we are going to use the 2nd one, the setRequiresAuthenticationRequestMatcher(), because it allows us to also change the HTTP method (the default is the POST method).
setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/signin", "GET" ));
We can do that by instantiating a bean of the filter inside our custom security configuration class and using the above method. Then we can apply the instance of the filter bean in our SecurityFilterChain. the rest of our CustomSecurityConfiguration remains the same:
Spring provides the necessary information about the UsernamePasswordAuthenticationFilter which in its turn extends the AbstractAuthenticationProcessingFilter class. So, you can take your time and dig deeper into the documentation and see all the capabilities those both filters offer us for further customizations.
In the next part, we are going to implement our very own custom filter, which will be based on the UsernamePasswordAuthenticationFilter.
A custom implementation of the UsernamePasswordAuthenticationFilter
What we are going to do
Analogously with what we did in my previous post, where we implemented a custom filter that extended the OncePerRequestFilter , here our implementation will extend the UsernamePasswordAuthenticationFilter. The functionality will be also dual-purpose.
Our custom filter will be used for both: Basic Authentication and JWT-based Authorization.
Both should use requests with an Authorization header. For the Basic Authentication, the value of the header should be a Base64 encoded token. For the JWT Authorization, that value should be a valid JWT token.
The basic endpoints are remaining the same. The following table provides the endpoints functionality:
Initial code
As starting point, we can use almost the same repo which was given as the final output of the previous post, except the CustomSecurityConfiguration class and the CustomRequestHeaderTokenFilter.
This is the initial CustomSecurityConfiguration class:
This is also the initial template of the CustomRequestHeaderTokenFilter
For your convenience, I provide this repo here, which can be the basis for our further customizations in this post.
Now we are ready. Let’s Start!
attemptAuthentication() for the Basic Authentication
You have probably noticed that the real authentication takes place inside the attemptAuthentication() method. This method is getting fired from within doFilter() method, whenevr the requiresAuthentication() invocation returns true.
Note that the requiresAuthentication() method returns true, only when a request concerns the endpoint and HTTP method is set with a RequiresAuthenticationRequestMatcher!
Please recall, that in our case here, this is about protecting the endpoint “/auth/signin” using the GET HTTP method. Also recall that the request should provide an Authorization header carrying a Base64 username:password encoded value (token) which actually represents a Basic Authentication schema.
So, the attemptAuthentication() is the method that we are going to override for our custom Basic Authentication. What actually, have to do is given in the following steps:
- Check if there is an Authorization header
- Check if the value of the Authorization header has the “Basic ” prefix.
- If yes, the extract the username and password values
- If there were correctly extracted, create an un-authenticated Authentication token with the username and password extracted
- Call the provided AuthenticationManager authenticate() method with the un-authenticated Authentication token
- Return the result from the authenticate() method. If the result is a fully authenticated Authentication object, then the successfulAuthentication() of the filter is getting fired.
- From with the successfulAuthentication() method, return back to the user, as a response, a valid JWT Bearer token
- If the authentication fails, then either the unsuccessfulAuthentication() method is getting fired, or a defined AuthenticationFailureHandler, so from within one of them we can send a failure message as a response to the user.
For our purpose we will use the corresponding static methods of our utility TokenUtils class, as well as the default settings provide as static constants by our SecurityConstants class. Moreover, we will also use a couple of private methods that will help us to keep our code a bit cleaner and closer to “Single Responsibility” principle.
So, gathering all the above together, this is how the first version of our custom filter becomes:
As you can see the isAuthorizationHeader() checks if an Authorization header concerns a Basic header or not. It takes as parameters the Authorization header value and the prefix (which in this case should be “BASIC ” and returns true if the header has a value that starts with the correct prefix. Otherwise, it returns false. Actually, this method is called by the other one, the getUnauthenticatedToken() method.
The job of the getUnauthenticatedToken() method is to return an un-authenticated Authentication object (token). This can be a token with an empty string as username (principal) and password, or the pair of username and password extracted from the Base64 encoded header value, by calling the static decodedBase64() method of our TokenUtils class.
We call the getUnauthenticatedToken() from our custom filter attemptAuthentication() method and with the value of the un-authenticated object we get back, we “feet our AuthenticationManager instance to do the real job of the Authentication.
After that, either the successfulAuthentication() or the unsuccessfulAuthentication() is getting fired, where we can send back to the user either a valid JWT Bearer token (if the authentication was successful), or an error message for an unsuccessful authentication, respectively.
The above functionality works pretty well, and you can test it using Postman.
The next step is to deal with Authorization via a valid JWT Bearer token. For this purpose, we are going to override the doFilter() method.
doFilter() – For JWT Bearer token Authorization
The doFilter() method is the “classic” method that is inherited by any Spring class based on the Filter interface. This method is getting fired initially, so we can use it for our purpose. The required coding steps and functionalities are given below:
- Get/extract the username from the JWT Bearer token claim(s) from the Authorization header provided
- Use a UserDetailsService to fetch a User object together with all her/his authorities (roles).
- If the User object returned from the UserDetailsService is not null, then use the User object to create an Authentication object
- Populate the created Authentication object – put it into the SecurityContext
- Finally, release the servlet to continue, by calling the super.doFilter() method. If the User has the appropriate authorities, the related Controller handler will send back the response Otherwise the default 403-Error (Forbidden) will be thrown.
The updated version of our custom filter becomes:
As you can see, for the purpose of using a UserDetailsService, we have created a private member and an additional public method, the setUserDetailsService() for setting it. We can call/use this method when we instantiate our custom filter from within our custom security configuration class.
Furthermore, we have also created another private method, the getUsernameFromBearerToken() method that does the job of extracting and validating a JWT Bearer token of the header (by also using the appropriate static method(s) of our utility TokenUtils class). Finally, it returns back either an empty string or the username extracted.
Below, also you can find the updated version of our custom security configuration class:
As you can see above, nothing is so special. We have only added (Autowired) our CustomUserDetailsService. Then, we passed it the instantiation method of our CustomRequestHeaderTokenFilter, by using the setUserDetailsService() we’ve seen previously.
Error Handling for Authentication and Authorization Failures
So far, we haven’t taken any care about error handling, since I have intentionally, left commented-out the unsuccessfulAuthentication() method of our custom filter. So, now it’s time to take some action.
However, if you have already used Postman to test our code, you are probably aware that when either an authentication or authorization error occurs, you get back, a respective error:
Note: Don’t get confused by the “Unauthorized” message for the 401 error. It actually means “Unauthenticated” (you can find a lot of posts debating on this). “Forbidden” and “Access Denied” on the other hand, generally mean that no user with appropriate authorities/roles can be found to access the resources of the /api/users endpoint.
The ExceptionTranslationFilter
But who is responsible for understanding the failures and sending un-authenticated or un-authorized responses? Well, this is the ExceptionTranslationFilter, which is automatically instantiated by the Spring framework and placed toward the end of the SecurityFilterChain. Its role is exactly this: to handle any AccessDeniedException and/or AuthenticationException thrown within the filter chain. If an AuthenticationException is detected, the filter launches the available AuthenticationEntryPoint instance and its commence() method. If an AccessDeniedException is detected for a user, the filter launches the available AccessDeniedHandler instance and its handle() method.
This Spring’s default behavior offers us the standard 401 and 403 errors, and this is pretty good when we want our error responses to provide just general error information.
On the other hand, when there is a need for our error information to be more specific, we have to proceed with more customizations. And there are a lot of approaches to that.
Handling authentication and authorization errors for our CustomRequestHeaderTokenFilter
The unsuccessfulAuthentication() method offered by the UsernamePasswordAuthenticationFilter that we use for our custom filter (and/or its parent class AbstractAuthenticationProcessingFilter) is among the first place of choices, and overriding it we can send back our custom error responses. However, this method -as its name shows- is limited to authentication failures only (and this is OK for a filter that is specialized for authentication). Thus, using this method leaves out the errors connected to authorizations (e.g. invalid or expired bearer tokens).
One solution, that one can think about, is to use a custom AccesDeniedHandler and apply it to our custom security configuration. This is fine, and this is what we will also do here. However, before proceeding to implement it, we have to stay a bit more and see what are some caveats with this.
The problem when applying a Custom AccessDeniedHandler with Bearer token-based authorizations
The main issue is that the AccesDeniedHandler, works only after an Authentication object is fully populated (i.e.: authenticated).
Thus, when a Bearer Token is null, expired, or invalid, no username can be extracted from the token’s claim(s). And, when we use a stateless application such as a REST API (where we do not preserve an authenticated Authentication object between requests), we cannot say who is this user (e.g. by calling a UserDetailsService) because we don’t even have a username. Therefore, we cannot populate/authenticate any Authentication object. In this case, no AccessDeniedHandler can get involved. There are dozens of StackOverflow issues asking why an AccessDeniedHandler is not getting executed, or it cannot be called, etc. So, bear in mind, that a common cause is that there is no fully populated or authenticated Authentication object in the SecurityContext (see other posts of mine related to the authentication process).
However, when a user can be extracted from the bearer token, and she/he also exists, but there are no appropriate authorities/roles, then an applied AccessDeniedHandler works fine, as expected.
So, when there is no authenticated Authentication object, there is no other way than capturing related errors as authentication failures. And this is true also in our case. We have to handle errors related to invalid, null, or expired Bearer Tokens as authentication errors.
Taking the approach to deal with all exceptions from within our custom filter and especially from within the unsuccessfulAuthentication() method, might be a bit messy (and can be turned into a real nightmare). A better solution seems to use (externally) a custom AuthenticationEntryPoint. The AbstractAuthenticationProcessingFilter inherits us with the setAuthenticationFailureHandler() setter method, which can be used to apply a custom AuthenticationFailureHandler, e.g. an AuthenticationEntryPointFailureHandler. The latter can adapt an AuthenticationEntryPoint into an AuthenticationFailureHandler.
The CustomAuthenticationEntryPoint
The general idea is to make a @Component annotated CustomAuthenticationEntryPoint class, mobilized with some specific properties/fields (with their setters and/or getters), that can be used from within our custom filter, to pass our specific error messages, status codes, and other error-related information. Below, is our custom implementation of the AuthenticationEntryPoint interface:
As you can see the CustomAuthenticationEntryPoint implements the AuthenticationEntryPoint interface and is really, is pretty simple. We use just 4 properties/methods that we can set for making an error response. Since the class is primarily being used for authentication errors we can initialize the values of those properties, accordingly, using the custom setInitValues() method. The important property is the customMessage that is being used to provide a more meaningful error cause with the error response body. Note that one can also use a special error object class, instead of using discrete properties as I do here. Besides this, you can see in the custom getReturnedJsonString(), that I also decided not to use Jackson or Gson for forming the response body as a JSON object. And I’m pretty sure, you can do those things on your own :-).
No more to say about our CustomAuthenticationEntryPoint class. However, we have to be aware of one more thing. And this is about the case of facing an unwished redirection to the /error endpoint.
Avoid unwished behavior of /error redirection
When the attemptAuthentication() has already been triggered but the authentication process fails, then an AuthenticationException is thrown. However, since we do not use either the unsuccessfulAuthentication() method or a configured AuthenticationFailureHandler, the default behavior (the default servlet behavior) triggers a redirection to the “/error” endpoint (for error page loading, if any). This, in its turn, triggers again our custom filter, and the doFilter() takes again action, but this time for the triggered “/error” endpoint. The result is to rise an unwished, custom authorization error (no Bearer token provided for /error endpoint), which is not about authentication, but it is captured by our custom Authentication point.
We can, of course, ignore/suspend any request concerning the “/error” endpoint inside the doFilter(), however, this is not the best approach. One solution is to return an authentication error response, by using/overriding the unsuccessfulAuthentication() method, but this means that we will not follow the DRY principle, since we have already the necessary code in our CustomAuthenticationEntryPoint.
So, a better solution is to use again our CustomAuthenticationEntryPoint for a custom AuthenticationFailureHandler, and more specifically an implementation for the AuthenticationEntryPointFailureHandler. We can do that by setting the custom AuthenticationFailureHandler using the appropriate setter, the setAuthenticationFailureHandler() method we’ve mentioned before. We can do that from within the same @Autowired method we are going to inject our custom AuthenticationEntryPoint:
Note, that the same can be done, when we instantiate our custom filter in our custom security configuration class:
Don’t get confused. The instance of our CustomAuthenticationEntryPoint injected that is assigned to the private property this.authPoint works for customization of the error messages concerning the Bearer Token authorization failures, while the one passed in the setter setAuthenticationFailureHandler() is for internal authentication error handling.
Our choice of selecting as an AuthenticationFailureHandler implementation, the AuthenticationEntryPointFailureHandler, allows in fact, to transfer the “duties” of the onAuthenticationFailure() contract method to the commence() method of an AuthenticationEntryPoint (our CustomAuthenticationEntryPoint in this case).
One last thing here is that our CustomAuthenticationEntryPoint, can be also used, as a general error authentication handler as well, when we apply it in our exceptionHandling configurer at our SecurityFilterChain bean.
The updated CustomRequestHeaderTokenFilter
Below you can find the updated (3rd) version of our CustomRequestHeaderTokenFilter:
As you can see, the code almost remains intact. The important is that we have used the injected CustomAuthenticationEntryPoint for bearer authorization failures. The last method added, the updateAuthorizationError() does this job. In the above example, we call it only from within the previous method, the getUsernameFromBearerToken() in the cases we have failures trying to validate a bearer token and/or extract the username from it.
Note, that in any failure case, we return an empty string as username to the calling doFilter(), which ensures that no user will be found via UserDetailsService. Thus, an authenticated error will be raised, but now we have acted proactively, to transform it into an authorization error. The comments I’ve included inside the code will also be useful.
The CustomAccessDeniedHandler
Now it’s time to create our custom AccessDeniedHandler. This is not necessary, however setting it up, allows us to have some uniformity in error responses.
Not something special here. We use it just to return an error response in the same way we did it in our CustomAuthenticationEntryPoint. Of course, whenever is necessary you can customize it more, on your own to fit your specific needs.
The updated CustomSecurityConfiguration class
Now, let’s see what our custom security configuration class becomes.
You can see above, not that big changes. We have just added the bean (using @Autowired annotation) of our CustomAuthenticationEntryPoint, and then we passed it into our custom filter instantiation method. As you can also see, we have added it, in our SecurityFilterChain exceptionHandling configurer, as a general authentication exception handler, as well. The same we did for our CustomAccessDeniedHandler.
So far so good. You can grab Postman and test our so far implementations. Everything seems OK.
Well, this is true, however, do you like the message in the error response when the authentication fails? To Me? No, I don’t!
I think we also have to be more specific, about the causes of an authentication failure. And I also think that, since we have our custom authentication handler (our CustomAuthenticationEntryPoint) in place, this is not a big deal. Actually, we can add some similar functionality, as we previously did for authorization errors. This is nothing but adding a method like the one below:
private void updateAuthenticatonError(String msg) { //this.authPoint.setInitValues(); this.authPoint.setPath(this.uri); this.authPoint.setCustomMessage(msg); }
After that, we can call it from wherever a failure can also cause the authentication failure, for instance, from within the method we are trying to deal with the Base64 encoded username:password (the getUnauthenticatedToken() method). Without other talking, below you can find the updated version of our CustomRequestHeaderTokenFilter:
Updates to our CustomAuthenticationProvider
As a final coding word, I would also suggest some changes in our CustomAuthenticationProvider. Actually, any errors thrown within it, can be captured, because we also have applied our CustomAuthenticationEntryPoint as a general authentication error handler in our SecurityFilterChain. So, for instance, when there is no user with the username provided, or the password does not match, instead of returning a null user object we can throw a related authentication exception. Below is the updated version of our CustomAuthenticationProvider.
I think, now is a bit better:
Note: Many times, for security reasons, you should avoid exposing real and detailed error messages, but this is up to you and your specific apps.
That said, we are toward the end of this post. Just a couple of more notices, I’d like also to make you aware of.
A couple of notes that you might be interested in
The requestMatcher for /auth/signin endpoint
One point that deserves to be mentioned here, is that we can avoid setting our protected authentication point using a “.requestMatchers()” configurer inside the SecurityFilterChain bean. This is because our CustomRequestHeaderTokenFilter, is the only one filter providing authentication, and we have also set the same endpoint with the filter’s setRequiresAuthenticationRequestMatcher() method (in the filter’s instantiation method). So, if you wish, you can remove the line:
.requestMatchers("/auth/signin").authenticated()
And then use Postman to test that nothing changes.
The setContinueChainBeforeSuccessfulAuthentication() method
Another one is the fact that after successful authentication, you can bypass any response from within the successfulAuthentication() method, which is the default behavior of the filter. You can do that by setting the filter’s setContinueChainBeforeSuccessfulAuthentication() method to false. This, for instance, can be useful in some cases, because it allows you to proceed to the controller’s endpoint handler, from where you can also send back a response.
That’s it!
Conclusion
Recap
We protected all the endpoints we are interested in. One endpoint is dedicated to new user registration and permits all requests. Another one is dedicated to authenticating registered users but it is also by itself protected with Basic Authentication. So, a registered user has to provide a Basic (Base64 encoded username:password string) token via an Authorization header. After successful authentication of the user, it receives a valid JWT Bearer token with a certain expiration time. Such a valid JWT Bearer token has to be provided (again via an Authorization header) for any next request accessing any other of our endpoints. According to the user’s authorities/roles the user will be authorized or not to access the resources of the specific endpoint. In this case, the controller handler methods respond with specific resources.
All the processes related to the authentication and authorization are handled by one and only one filter, which we implemented as a custom filter extending the UsernamePasswordAuthenticationFilter.
We finally saw, how you can deal with the errors caused during the authentication and authorization process using a custom AuthenticationEntryPoint, and a custom AccessDeniedHandler.
Besides, we’ve also made some references, when it was deemed necessary, concerning other functionalities of the UsernamePasswordAuthenticationFilter, as well as other related Spring subjects.
Possible next steps you can take
You can access the related repo here, and then you can work on your own for further development and customizations. For instance, what you can additionally do, is add or change the handler methods in the controllers.
Moreover, you can also use a @ControllerAdvice error handler (with @ExceptionHandler methods) to deal with failures specific to controllers.
Finally, you can easily adapt the provided repo to match your database needs by just adding database connection settings in your application.properties files, and adapting the repositories (and/or other parts) accordingly.
That’s it for now! If you enjoyed this post, I would appreciate having feedback from you.!
Thanks for reading? and stay tuned!