If you've used the Google to learn about how to implement AuthenticationFailureHandler with Spring Security, you might have noticed that most of the results deal with embedded login forms. But what if you're using security with REST?

Well, happily enough, there's a way to use AuthenticationFailureHandler even if you're using JWT security with a REST service. In this guide I'll show you how to do it.

Of course, you can always go to straight to the code if that's how you roll.

Otherwise, stay awhile.

The Business Requirements

Your boss Smithers walks into your office. He's clearly disappointed.

"That CRM app you're working on," he says, "I noticed it just delivers a status code of 401 with authentication errors. It doesn't deliver a message! I need a message!"

Smithers starts huffing.

"Update the code so you send a message back with the 401 status code."

He walks out of your office still huffing.

Refactoring Time

As Smithers noted, you've already got security in place. You've implemented a solution that enables users to provide a username and password in exchange for a JSON Web Token (JWT). 

On subsequent requests, users will send the token instead of logging in all over again. That's standard fare for this type of setup.

But also as Smithers noted, when the authorization fails for any reason, the application just sends back a status code (401 or Unauthorized) with no corresponding message. That's not very user-friendly.

It's time to make some changes.

But first, let me explain exactly how security works in the application so you can follow along.

Fun With Filters

We use filters here.

If you're unfamiliar with filters in a Spring security context, they give you the ability to intercept requests and perform some kind of processing.

Here, the application uses filters to enable users to authenticate.

Remember, though, they can authenticate in one of two ways:

  • With a username and password
  • With a JWT

So there are two (2) filters in place. The first one checks for a JWT and grants authorization if it's valid. The second one checks for a valid username/password combo.

The whole thing is configured in the WebSecurityConfig class and looks like this:

	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {		
		httpSecurity
		    .cors().and()
		    .csrf().disable()
		    .addFilter(bearerTokenAuthenticationFilter())
		    .addFilter(credentialsAuthenticationFilter())
		    .authorizeRequests()
            .anyRequest().access("hasAuthority('CAREYDEVELOPMENT_CRM_USER')").and()
		    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
	}

See those two addFilter() methods? That's where the filters get added.

It's important to understand that here because you'll add the AuthenticationFailureHandler objects to those filters. That way, when authentication fails in either one of the filters, you can handle it with a response code and a nice message back to the user.

Now let's take a look at how you do that.

Prepping for Failure

Create a new method in that WebSecurityConfig class. Make it look like this:

	private AuthenticationFailureHandler authenticationFailureHandler() {
	    return (request, response, ex) -> {
	        response.setStatus(HttpStatus.UNAUTHORIZED.value());
	        response.setContentType(MediaType.APPLICATION_JSON.toString());
	        response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
	        
	        ResponseWriterUtil.writeErrorResponse(response, ex.getMessage());			
	    };
	}

In a nutshell, that method constructs an instance of AuthenticationFailureHandler using a lambda expression to implement its only method: onAuthenticationFailure().

Here the code sets the response status to "Unauthorized" or a 401 status code. Then, it writes the exception message back to the body.

Now keep in mind: that exception is a type of AuthenticationException. So by the time the code gets here, the application already knows that something went wrong because of an authentication issue.

It's up to the individual authentication providers to set the exception message correctly. For example, if the token is expired, then the provider should throw AuthenticationException with a message that reads: "Token expired."

And that's exactly what happens here.

The lambda block also sets the content type and character encoding of the response. That's par for the course for these kinds of manually encoded responses.

Next, the code above uses a couple of new classes to write the message to the response body.

Here's the class that represents the message itself:

public class ResponseStatus {

	public static enum StatusCode { OK, ERROR };
	
	private StatusCode statusCode;
	private String message;
	
	public StatusCode getStatusCode() {
		return statusCode;
	}
	public void setStatusCode(StatusCode statusCode) {
		this.statusCode = statusCode;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
}

That's nothing more than a String message associated with a status code. In the case of an authentication failure, that status code will get set to ERROR.

And here's the class that actually writes the output to the response:

    private static final Logger LOG = LoggerFactory.getLogger(ResponseWriterUtil.class);
	
    public static void writeErrorResponse(HttpServletResponse response, String message) {
        ResponseStatus status = new ResponseStatus();
        status.setStatusCode(ResponseStatus.StatusCode.ERROR);
        status.setMessage(message);
		
        try (PrintWriter writer = response.getWriter()) {
            String json = new ObjectMapper().writeValueAsString(status);
			
            writer.write(json);
			writer.flush();
        } catch (IOException ie) {
            LOG.error("Problem writing output to response!", ie);
        }
    }

Nothing too complicated there. It takes the String message that gets passed in and transforms it into a ResponseStatus object. That's the class you just looked at.

Then it gets the PrintWriter from the HttpServletResponse object and writes a message to it. That message is transformed into JSON thanks to Jackson.

And that's pretty much it.

Now, it's time to make use of the AuthenticationFailureHandler object.

Manhandling

Back in WebSecurityConfig add two new methods that will instantiate your filters:

	private BearerTokenAuthenticationFilter bearerTokenAuthenticationFilter() throws Exception {
	    BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(authenticationManager());
	    filter.setAuthenticationFailureHandler(authenticationFailureHandler());

	    return filter;
	}
	
	
	private CredentialsAuthenticationFilter credentialsAuthenticationFilter() throws Exception {
	    CredentialsAuthenticationFilter filter = new CredentialsAuthenticationFilter(authenticationManager());
	    filter.setAuthenticationFailureHandler(authenticationFailureHandler());

	    return filter;
	}

Both of those methods don't just create the filter objects, they also set the AuthenticationFailureHandler for both filters. They set it to the object you created a couple of sections back.

You Must Test

Now it's time to test it out. Save everything and launch the Spring Boot application.

Now, go into Postman and use a bearer token that's intentionally expired. Then, try to access the /user/me endpoint.

For the record: that endpoint returns details about the currently logged-in user.

But let's see what happens if you try it with an expired token:

 

Perfect! It delivered a 401 response with the message you're looking for.

Now try to login with bad credentials and see what happens:

 

Excellent. Once again, it works as expected.

Wrapping It Up

Congratulations! You've now implemented a Spring security solution that uses AuthenticationFailureHandler.

Now it's up to you to do some refactoring. Maybe you'd like to improve on that ResponseStatus class. Or perhaps you'd like to provide more details in your error messages.

The possibilities are endless.

As always, feel free to grab the code on GitHub and mess around with it as you see fit.

Have fun!