intermediate~2h

Custom AuthenticationProvider & Exception Handling

The default DaoAuthenticationProvider covers the common case. This chapter is what to do when authentication needs custom logic — and how to handle it cleanly when authentication fails.

Learning objectives

  • Beginner: State what contract an AuthenticationProvider must implement.
  • Intermediate: Implement a custom AuthenticationProvider for a non-standard authentication rule.
  • Advanced: Implement custom AuthenticationEntryPoint and AccessDeniedHandler beans that return consistent JSON errors instead of Spring's default HTML pages.

🌱 BEGINNER STORY

Imagine a company's hiring process has one HR coordinator (the AuthenticationManager) who doesn't personally verify every kind of credential — instead they hand the candidate off to the right SPECIALIST interviewer: a coding-test proctor for engineers (DaoAuthenticationProvider for username/password), a portfolio reviewer for designers (a custom OTP provider), or a background-check agency for security roles (an LDAP provider). Each specialist knows exactly one verification method deeply. If you want to add a brand-new hiring method — say, verifying candidates via a one-time SMS code — you don't rewrite the HR coordinator; you just add a NEW specialist (a custom AuthenticationProvider) to the panel.

The default DaoAuthenticationProvider handles the standard 'look up user, compare hashed password' flow perfectly well. You write your OWN AuthenticationProvider when you need custom verification logic that doesn't fit that mold — common real-world cases:

  • One-Time-Password (OTP) login via SMS/email instead of (or in addition to) a static password.

  • Verifying credentials against an external legacy system or third-party identity provider with a non-standard protocol.

  • Adding extra business rules during authentication — e.g., blocking login entirely outside business hours for certain account types, or enforcing device-fingerprint checks.

  • Combining multiple factors (password + a hardware token code) in a single custom verification step.

Every AuthenticationProvider implements two methods:

  • authenticate(Authentication): receives the unauthenticated token, performs verification, and returns a NEW, fully-populated (authenticated) Authentication object on success — or throws an AuthenticationException on failure.

  • supports(Class<?> authentication): tells ProviderManager whether this provider can handle a given Authentication implementation class — this is how ProviderManager picks the right specialist without hardcoding logic.

@Component

public class EazyBankUsernamePwdAuthenticationProvider implements AuthenticationProvider { private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; public EazyBankUsernamePwdAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService = userDetailsService; this.passwordEncoder = passwordEncoder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String pwd = authentication.getCredentials().toString(); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (!passwordEncoder.matches(pwd, userDetails.getPassword())) { throw new BadCredentialsException("Invalid password!"); } // Extra custom business rule example: if (isLoginBlockedForMaintenance(username)) { throw new DisabledException("Login temporarily disabled for maintenance window"); } return new UsernamePasswordAuthenticationToken( username, pwd, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } private boolean isLoginBlockedForMaintenance(String username) { return false; // custom business logic goes here } }

Because it's a @Component, Spring auto-registers it, and ProviderManager will automatically include it in the list of providers it tries — you typically don't even need to wire it manually, though you CAN explicitly build an AuthenticationManager with a specific provider list for full control.

🌱 BEGINNER STORY

Think of a bank's security office that doesn't just check ID cards at the door — they also keep a logbook: every successful entry AND every rejected attempt gets written down, with a timestamp. If the same person fails to badge in 5 times in a row, the security office automatically locks that badge until a manager reviews it. Spring Security gives you exactly this logbook for free: every authentication attempt — success or failure — is broadcast as a Spring ApplicationEvent, and you can 'subscribe' to it anywhere in your application without touching the authentication flow itself.

Figure: ProviderManager publishes a Spring ApplicationEvent on every authentication attempt; any @EventListener can react without being wired into the authentication flow itself.

Whenever ProviderManager processes an authentication attempt, it publishes one of several AbstractAuthenticationEvent subtypes — AuthenticationSuccessEvent on success, or a specific failure event like AuthenticationFailureBadCredentialsEvent, AuthenticationFailureLockedEvent, AuthenticationFailureDisabledEvent, matching the exact exception that was thrown. This decouples 'what happens when login fails' (business/security response) from 'how login is verified' (the AuthenticationProvider) — you never need to touch your AuthenticationProvider code to add auditing or lockout logic.

@Component

public class AuthenticationAuditListener { private final LoginAttemptRepository loginAttemptRepository; public AuthenticationAuditListener(LoginAttemptRepository loginAttemptRepository) { this.loginAttemptRepository = loginAttemptRepository; } @EventListener public void onSuccess(AuthenticationSuccessEvent event) { String username = event.getAuthentication().getName(); loginAttemptRepository.recordSuccess(username, Instant.now()); } @EventListener public void onBadCredentials(AuthenticationFailureBadCredentialsEvent event) { String username = event.getAuthentication().getName(); int recentFailures = loginAttemptRepository.recordFailureAndCount(username, Instant.now()); if (recentFailures >= 5) { loginAttemptRepository.lockAccount(username); // isAccountNonLocked() will now return false } } }

Because lockAccount() flips a flag that your UserDetails.isAccountNonLocked() implementation reads, the VERY NEXT login attempt (even with the correct password) will be rejected by DaoAuthenticationProvider with a LockedException — all without a single change to your AuthenticationProvider or UserDetailsService code. This is a clean, decoupled way to implement 'lock after N failed attempts,' a near-universal real-world security requirement.

Figure: ExceptionTranslationFilter routes authentication failures to AuthenticationEntryPoint (401) and authorization failures to AccessDeniedHandler (403).

ExceptionTranslationFilter sits high up in the filter chain and acts as a catch-all for two specific exception types thrown anywhere downstream:

  • AuthenticationException: 'I don't know who you are, or your credentials were rejected' -> delegated to AuthenticationEntryPoint -> typically HTTP 401.

  • AccessDeniedException: 'I know who you are, but you can't do this' -> delegated to AccessDeniedHandler -> typically HTTP 403.

@Component

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(MediaType.APPLICATION_JSON_VALUE); Map<String, Object> body = Map.of( "timestamp", LocalDateTime.now().toString(), "status", 401, "error", "Unauthorized", "message", authException.getMessage(), "path", request.getRequestURI()); new ObjectMapper().writeValue(response.getOutputStream(), body); } } @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(MediaType.APPLICATION_JSON_VALUE); Map<String, Object> body = Map.of( "timestamp", LocalDateTime.now().toString(), "status", 403, "error", "Forbidden", "message", ex.getMessage(), "path", request.getRequestURI()); new ObjectMapper().writeValue(response.getOutputStream(), body); } } // wiring: http.exceptionHandling(ex -> ex .authenticationEntryPoint(customAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler));

Without these, Spring Boot's default error handling returns a generic HTML Whitelabel Error Page — completely unusable for a REST API client (like an Angular SPA or mobile app) that expects a consistent JSON error contract.

⚠ What trips teams up

Throwing a generic RuntimeException inside a custom AuthenticationProvider (instead of a proper AuthenticationException subclass) bypasses ExceptionTranslationFilter entirely and results in a raw 500 Internal Server Error instead of a clean 401 — always throw/extend AuthenticationException. Ordering multiple AuthenticationProvider beans matters when more than one 'supports()' the same Authentication type — ProviderManager tries them in the order they're registered/declared, and the first one to either succeed or throw a 'hard' exception (like BadCredentialsException, as opposed to just declining) short-circuits the rest. Custom exception handlers that log the raw exception message directly back to the client can leak internal details (e.g., 'User with email x@y.com not found' actually reveals whether an email is registered at all — a user-enumeration vulnerability). Prefer a generic 'Invalid username or password' message externally, and log full detail only server-side.

Want a visual for this concept?

Generate a diagram tailored to “Custom AuthenticationProvider & Exception Handling” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Session Management: Timeout, Concurrency & Fixation← Back to all Spring Security chapters