Recently, I have undertaken Spring Security authentication logic implementation in production environments for the first time. Fundamental functionalities such as login, registration, and authorization proved relatively straightforward owing to abundant tutorials and documentation. However, implementing seemingly trivial auxiliary requirements presented greater challenges. Among these, the troubleshooting process culminating in error handling for multiple consecutive incorrect password entries merits documentation.
Initially, I attempted to utilize the
AuthenticationFailureHandlerinterface implementation created for REST API development.- (The default FailureHandler proves unsuitable due to login page redirection logic. Spring Security’s default form authentication necessitates failure handler implementation that returns 401 HTTP status without redirection for REST API compatibility.)
- This FailureHandler would perform counting upon each login failure, displaying error messages upon exceeding n attempts. As the class responsible for post-authentication failure, it appeared more appropriate than ProviderManager.
Consequently, I commenced logic implementation resembling the following code:
| |
(Assuming count storage in database columns. Code reads request to extract username for service layer transmission for count increment)
- The issue: Request reading proved impossible
- POST-transmitted “application/json” body reading via
request.reader.lines().collect(Collectors.joining())produced empty results. Alternative methodologies yielded identical outcomes… - Pre-FailureHandler request reading verification via debugging confirmed successful reading
- The issue originated from
AbstractAuthenticationProcessingFilterinterface implementations containing request reading logic for authentication, with read requests proving unrereadable subsequently- → Why???
- HttpServletRequest’s InputStream prohibits repeated reading
- POST-transmitted “application/json” body reading via
Contemplated Solutions
Servlet Filter layer request InputStream reading with HttpServletRequestWrapper implementation enabling re-reading
- → Layer boundary violation, with difficulty comprehending impacts on other project code. Not implemented
Share username between initial request reading implementation and FailureHandler? Read username from SecurityContextHolder in FailureHandler
- → Nonexistent.. SecurityContextHolder stores authenticated users, not unauthenticated usernames
ProviderManager assumes partial failure logic responsibility ✅
Override ProviderManager, incrementing login failure count upon authentication failure (password mismatch). Upon exceeding failure count, throw AuthenticationException with exception message → FailureHandler writes exception appropriately for transmission.
More specifically, create AuthenticationProvider class extending
AbstractUserDetailsAuthenticationProvider, with internal implementation closely following defaultDaoAuthenticationProvider, inserting desired logic into authentication failure sections.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25@Throws(AuthenticationException::class) override fun additionalAuthenticationChecks( userDetails: UserDetails, authentication: UsernamePasswordAuthenticationToken ) { if (authentication.credentials == null) { logger.debug("Failed to authenticate since no credentials provided") throw BadCredentialsException( messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials") ) } val presentedPassword = authentication.credentials.toString() if (!this.passwordEncoder.matches(presentedPassword, userDetails.password)) { logger.debug("Failed to authenticate since password does not match stored value") handleAuthenticationFail(userDetails.username) // Newly added throw BadCredentialsException( messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials") ) } } private fun handleAuthenticationFail(username: String) { // Implementation // Increment count; throw BadCredentialsException if failures exceed time threshold }Response processing in FailureHandler.
For example:
1 2 3 4 5httpStatus: 401 { "message": "Login failed 5 or more times" }FailureHandler implementation for this response:
1 2 3 4 5 6 7 8 9 10response?.contentType = MediaType.APPLICATION_JSON_VALUE response?.status = HttpStatus.UNAUTHORIZED.value() response?.writer?.append( objectMapper().writeValueAsString( FailureResponse(exception?.message) ) ) data class FailureResponse(val message: String?)
This exposition may receive updates. While operational verification succeeded, lingering uncertainty regarding superior methodologies persists.
Full code will be shared if comprehensive tutorials are authored subsequently.
** Superior methodologies: please comment 🙏