You have two AuthenticationProviders supporting the same token: a primary and a legacy fallback. A user's account is locked. Why might returning DisabledException/LockedException versus BadCredentialsException from the primary produce very different behavior, and how does that inform provider ordering and exception design?
answer
- AccountStatus/InternalService = fatal short-circuit
- BadCredentials = continuable -> fallback runs
- wrong exception can let fallback bypass a lock
- order authoritative provider first
- exception type == control flow + audit events
basics
~20 sIf the primary throws an AccountStatusException (like LockedException), ProviderManager stops the chain immediately, so the legacy provider never runs and the user sees 'account locked'. If it throws BadCredentialsException, ProviderManager keeps going and the legacy provider may authenticate or reject, changing the outcome and the audit trail.
solid answer
~40 sProviderManager treats AccountStatusException and InternalAuthenticationServiceException as fatal: the moment the primary throws LockedException/DisabledException, iteration halts and that exception propagates, so the legacy fallback is never consulted. Any other AuthenticationException (BadCredentialsException) is non-fatal — it's remembered and the chain continues to the legacy provider, which could authenticate the user (masking the lock) or reject differently. This has real consequences: the user's error message, whether lockout policy is honored across mechanisms, and which AuthenticationEventPublisher events fire (affecting audit and brute-force listeners). Design implications: order the authoritative provider first; ensure account-state checks throw the correct fatal exception type so a fallback can't bypass a lock; wrap infrastructure failures in InternalAuthenticationServiceException so a DB outage doesn't degrade into a misleading BadCredentials from a fallback. Exception type is effectively control flow here, so choose it deliberately.
code
java · 25 lines// Primary provider: surface account state as a FATAL exception so no fallback can bypass it
class PrimaryAuthProvider implements AuthenticationProvider {
private final UserStore store;
PrimaryAuthProvider(UserStore store) { this.store = store; }
@Override public Authentication authenticate(Authentication auth) {
Account a;
try {
a = store.load((String) auth.getPrincipal());
} catch (DataAccessException ex) {
// Fatal: stops the chain instead of degrading to the legacy store on an outage
throw new InternalAuthenticationServiceException("User store unavailable", ex);
}
if (a.isLocked()) {
throw new LockedException("Account locked"); // AccountStatusException -> fatal, no fallback
}
if (!a.passwordMatches((String) auth.getCredentials())) {
throw new BadCredentialsException("Bad credentials"); // continuable -> legacy may still try
}
return new UsernamePasswordAuthenticationToken(a.principal(), null, a.authorities());
}
@Override public boolean supports(Class<?> c) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(c);
}
}go deeper
Understand that a locked account throws a specific exception and the user is told they're locked.
Know that AccountStatusException stops the chain while BadCredentialsException lets it continue.
Explain how ordering plus exception type together determine security behavior across multiple providers, including audit events.
Treat exception selection and provider ordering as an API/security-design decision; prevent fallback lock-bypass and outage-masking, and reason about the event/audit surface holistically.
## The mechanism that makes this matter `ProviderManager` classifies exceptions thrown by providers into two buckets: - **Fatal / short-circuiting**: `AccountStatusException` (parent of `DisabledException`, `LockedException`, `AccountExpiredException`, `CredentialsExpiredException`) and `InternalAuthenticationServiceException`. When a provider throws one of these, `ProviderManager` **stops iterating immediately** and rethrows it. No later provider runs; the parent is not consulted. - **Continuable**: any other `AuthenticationException` (typically `BadCredentialsException`, `UsernameNotFoundException`). It's stored as `lastException` and the loop **continues** to the next supporting provider. This single design choice is why *exception type is control flow*. ## The scenario Two providers support `UsernamePasswordAuthenticationToken`: a **primary** (say the modern DAO provider) listed first, and a **legacy** fallback (an old datastore) listed second. A user is **locked**. - **If the primary throws `LockedException`** (an `AccountStatusException`): the chain short-circuits. The user gets a definitive 'account locked' response; the legacy provider is never asked; a lockout/audit listener sees a clear status event. Correct. - **If the primary instead throws `BadCredentialsException`** (e.g. because it collapses all failures into 'bad credentials', or `hideUserNotFoundExceptions` masking): the chain **continues** to the legacy provider. Now: - If legacy still has the old (unlocked) record and the password matches, it **authenticates the user — bypassing the lock**. A serious security defect. - If legacy rejects, the user sees a generic 'bad credentials' rather than 'locked', harming UX and hiding the real cause. ## Design implications 1. **Order the authoritative provider first.** The provider that owns account-state truth should run before any fallback so its fatal exceptions short-circuit the chain. 2. **Throw the correct exception type deliberately.** Account-state problems must surface as `AccountStatusException` subtypes precisely so a fallback cannot silently re-authenticate a locked/disabled user. If you deliberately mask user existence with `BadCredentialsException`, understand you are also making that failure *continuable*. 3. **Wrap infrastructure failures in `InternalAuthenticationServiceException`.** If the primary's datastore is down, throwing this (fatal) type prevents the request from silently falling through to a stale legacy store and producing an inconsistent or misleading result. A raw `RuntimeException` would propagate too, but the typed wrapper keeps semantics clear and is recognized as fatal. 4. **Mind the audit/event surface.** `ProviderManager` publishes via `AuthenticationEventPublisher`: `AuthenticationFailureBadCredentialsEvent`, `...LockedEvent`, `...DisabledEvent`, etc. Brute-force and lockout listeners key off these; a fallback that swallows a lock or reclassifies it corrupts that signal. 5. **Reason about credential erasure and principal consistency** if the two providers return different principal shapes; whichever wins produces the `Authentication` stored in context. ## When to reach for this reasoning Any time you compose multiple providers for the **same** token type — migration scenarios (old + new store), multi-tenant auth, or layered mechanisms. The short-circuit rule means the *combination* of ordering and exception types, not each provider in isolation, defines the security-relevant behavior. Treat exception selection as an API-design decision, not an afterthought.
- If both providers are legitimate and you WANT the fallback to run on a bad password but NOT on a lock, how do you achieve it?Have the authoritative provider throw a continuable BadCredentialsException for wrong passwords (so the fallback is consulted) but a fatal LockedException/DisabledException for account state (so the chain short-circuits and the fallback can't bypass the lock). The exception-type classification gives you exactly this selective behavior.
- How does this interact with brute-force lockout listeners?ProviderManager publishes typed events via AuthenticationEventPublisher (e.g. AuthenticationFailureBadCredentialsEvent, AuthenticationFailureLockedEvent). Listeners that count failures or enforce lockouts rely on the correct event; if a fallback reclassifies or swallows the failure, the counters and lockout state become inconsistent.
saying these in an interview costs you the question
- Believing all AuthenticationExceptions stop the chain equally
- Assuming provider order is irrelevant when both support the same token
- Treating exception type as cosmetic rather than control flow
- Letting a DB outage fall through to a stale fallback store instead of throwing InternalAuthenticationServiceException