Spring Security Custom AuthenticationFailureHandler: The Complete Guide
Learn how to implement a custom AuthenticationFailureHandler in Spring Security 6.2.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Basic knowledge of Spring Boot and Spring Security
- ✓Familiarity with Java 17+ and lambda expressions
- ✓Understanding of HTTP status codes and REST API design
- Use AuthenticationFailureHandler to customize login failure responses
- Implement SimpleUrlAuthenticationFailureHandler or write your own
- Inject failureHandler into SecurityFilterChain configuration
- Handle exceptions like BadCredentialsException, LockedException, etc.
- For REST APIs, return JSON instead of redirecting
Think of an AuthenticationFailureHandler like a bouncer at a club who doesn't just say 'no entry' but explains why: wrong password, account locked, etc. It lets you customize the rejection message instead of just slamming the door.
I've lost count of how many times I've seen production apps where users get a generic 'Login failed' message after mistyping their password. That's not just bad UX — it's a security anti-pattern. Spring Security's default behavior redirects to a login error page, but in modern REST APIs, you need JSON responses, rate limiting hints, and account lockout feedback.
Enter the AuthenticationFailureHandler. It's the hook that lets you control exactly what happens when authentication fails. Whether you're building a traditional form-login app or a stateless JWT-based API, you need to handle failures gracefully.
In this guide, I'll show you how to implement a custom AuthenticationFailureHandler from scratch. We'll cover the common pitfalls I've seen in production — like forgetting to handle account lockouts or accidentally leaking sensitive information in error messages. You'll also get a production debugging guide and a real incident story that'll save you from a 2 AM pager call.
Let's dive in.
Why You Need a Custom AuthenticationFailureHandler
Spring Security's default failure handling is designed for traditional server-rendered form login. It redirects to /login?error and that's it. For modern apps—especially REST APIs—that's useless. You need to return structured JSON with appropriate HTTP status codes, maybe include rate-limiting headers, or even trigger account lockout notifications.
A custom AuthenticationFailureHandler gives you full control over the response. You can differentiate between bad credentials, expired passwords, locked accounts, or disabled accounts. Each can have a different response body and status code. For example, a locked account should return 423 Locked, not just 401.
Here's a concrete scenario: a SaaS billing system. If a user's account is disabled due to non-payment, you want to return a 402 Payment Required status. The default handler can't do that. With a custom handler, you can map exceptions to business-relevant responses.
Another use case: security auditing. You can log every authentication failure with the exact reason, IP address, and timestamp. The default handler doesn't log anything useful.
Bottom line: if you're building anything beyond a simple demo app, you need a custom AuthenticationFailureHandler.
Setting Up the Security Configuration
Once you have your handler, you need to wire it into Spring Security's filter chain. In your SecurityFilterChain bean, use the .failureHandler() method on the formLogin() or httpBasic() configuration.
A common mistake I see is developers forgetting to call .failureHandler() and wondering why their handler isn't invoked. Another pitfall: if you're using multiple authentication mechanisms (e.g., form login and JWT), you need to set the handler on each one separately.
Here's a complete configuration for a stateless REST API using form login only for authentication. Note that we disable CSRF and set the session creation policy to STATELESS.
Handling Different Exception Types Gracefully
Not all authentication failures are the same. Spring Security throws different exceptions for different scenarios:
- BadCredentialsException: wrong username/password
- LockedException: account locked (e.g., too many attempts)
- DisabledException: account disabled
- AccountExpiredException: account expired
- CredentialsExpiredException: password expired
- InsufficientAuthenticationException: missing authentication (e.g., no token)
Your handler should differentiate these to provide appropriate feedback. For example, a locked account should return 423 Locked, an expired password should return 426 Upgrade Required, and a disabled account should return 403 Forbidden.
But be careful: returning specific error messages can be a security risk. Attackers can use them to enumerate valid usernames (e.g., 'User not found' vs 'Bad credentials'). The safest approach is to return a generic 'Invalid username or password' for all exceptions except account state issues that don't reveal user existence, like 'Account locked' (which implies the account exists but is locked).
Here's an enhanced handler that maps exceptions to appropriate status codes and messages.
What the Official Docs Won't Tell You
Spring Security's documentation is comprehensive but misses several critical gotchas that I've learned the hard way.
1. The Handler is Not Called for All Failures The AuthenticationFailureHandler only fires for authentication attempts that go through the form login filter. If you're using HTTP Basic, the handler is different (AuthenticationEntryPoint). If you're using a custom filter, you need to invoke the handler manually. I once spent hours debugging why my handler wasn't called for JWT token failures — it was because the exception was thrown in a different filter.
2. Exception Messages Are Localized Spring Security uses message resources for exception messages. If you're relying on exception.getMessage() to detect the reason, the message might be in a different language depending on the locale. Always check the exception type, not the message string.
3. The Handler Can Be Called Multiple Times If you have multiple authentication mechanisms (e.g., form login and remember-me), the handler can be called more than once. Ensure your handler is idempotent — writing to the response twice will cause an IllegalStateException.
4. Stateless Session Pitfall When you set session creation policy to STATELESS, Spring Security still creates a session for the authentication failure. This can be a performance issue under heavy load. To avoid this, use SecurityContextHolderFilter and ensure no session is created.
5. Exception Translation Filter There's an ExceptionTranslationFilter that catches AuthenticationException and delegates to the AuthenticationEntryPoint. If you have both a custom failure handler and a custom entry point, make sure they don't conflict. The failure handler is for form login; the entry point is for requests that require authentication.
6. Testing is Tricky Mocking HttpServletRequest and HttpServletResponse is painful. Use Spring's MockHttpServletRequest and MockHttpServletResponse from spring-test. Also, remember that the handler runs in the security filter chain, so you need to test the full security configuration, not just the handler in isolation.
Integrating with Spring Security's Exception Handling
Your custom AuthenticationFailureHandler is just one piece of the puzzle. Spring Security also has AuthenticationEntryPoint (for unauthenticated requests) and AccessDeniedHandler (for unauthorized requests). These three work together to provide a comprehensive error response strategy.
- AuthenticationFailureHandler: for failed login attempts
- AuthenticationEntryPoint: for requests without credentials (returns 401)
- AccessDeniedHandler: for authenticated users without required role (returns 403)
Here's how to configure all three in a single SecurityFilterChain.
Testing Your Custom AuthenticationFailureHandler
Testing authentication failure handling is often overlooked. Developers test the happy path (successful login) but ignore failure scenarios. Here's how to test your custom handler using Spring Boot's test utilities.
You'll need to mock the authentication attempt and verify the response. Use MockMvc to simulate a login request with invalid credentials.
The 500 Error That Locked Everyone Out
- Never rely on default handlers for REST APIs — they assume form-login redirects.
- Always test failure scenarios, not just happy paths.
- Use custom handlers to return appropriate HTTP status codes (401 vs 403).
- Include error codes for client-side handling, not just messages.
- Log authentication failures with enough context to debug but without exposing secrets.
curl -v -X POST http://localhost:8080/login -d 'username=test&password=wrong'Check logs for 'AuthenticationFailureHandler'| File | Command / Code | Purpose |
|---|---|---|
| CustomAuthenticationFailureHandler.java | public class CustomAuthenticationFailureHandler implements AuthenticationFailure... | Why You Need a Custom AuthenticationFailureHandler |
| SecurityConfig.java | @Configuration | Setting Up the Security Configuration |
| EnhancedAuthenticationFailureHandler.java | public class EnhancedAuthenticationFailureHandler implements AuthenticationFailu... | Handling Different Exception Types Gracefully |
| FullSecurityConfig.java | @Configuration | Integrating with Spring Security's Exception Handling |
| AuthenticationFailureHandlerTest.java | @SpringBootTest | Testing Your Custom AuthenticationFailureHandler |
Key takeaways
Interview Questions on This Topic
How would you implement a custom AuthenticationFailureHandler that returns different HTTP status codes for different exceptions?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Spring Security. Mark it forged?
4 min read · try the examples if you haven't