From e16ce57fbb30eb0b10d04706d6f8730ee72057e4 Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Wed, 26 Jun 2024 14:03:44 -0300 Subject: [PATCH] Use AuthenticationFailureHandler instead of @ControllerAdvice Closes gh-15305 --- .../authentication/password-storage.adoc | 85 ++++++++++--------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc index c10967b7c44..2bb4006f502 100644 --- a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc +++ b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc @@ -602,7 +602,11 @@ To facilitate that, Spring Security provides integration with the https://haveib You can either use the `CompromisedPasswordChecker` API by yourself or, if you are using xref:servlet/authentication/passwords/dao-authentication-provider.adoc[the `DaoAuthenticationProvider]` via xref:servlet/authentication/passwords/index.adoc[Spring Security authentication mechanisms], you can provide a `CompromisedPasswordChecker` bean, and it will be automatically picked up by Spring Security configuration. -.Using CompromisedPasswordChecker as a bean +By doing that, when you try to authenticate via Form Login using a weak password, let's say `123456`, you will receive a 401 or be redirected to the `/login?error` page (depending on your user-agent). +However, just a 401 or the redirect is not so useful in that case, it will cause some confusion because the user provided the right password and still was not allowed to log in. +In such cases, you can handle the `CompromisedPasswordException` via the `AuthenticationFailureHandler` to perform your desired logic, like redirecting the user-agent to `/reset-password`, for example: + +.Using CompromisedPasswordChecker [tabs] ====== Java:: @@ -615,8 +619,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) - .formLogin(withDefaults()) - .httpBasic(withDefaults()); + .formLogin((login) -> login + .failureHandler(new CompromisedPasswordAuthenticationFailureHandler()) + ); return http.build(); } @@ -624,6 +629,25 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CompromisedPasswordChecker compromisedPasswordChecker() { return new HaveIBeenPwnedRestApiPasswordChecker(); } + +static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler( + "/login?error"); + + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + if (exception instanceof CompromisedPasswordException) { + this.redirectStrategy.sendRedirect(request, response, "/reset-password"); + return; + } + this.defaultFailureHandler.onAuthenticationFailure(request, response, exception); + } + +} ---- Kotlin:: @@ -636,8 +660,9 @@ open fun filterChain(http:HttpSecurity): SecurityFilterChain { authorizeHttpRequests { authorize(anyRequest, authenticated) } - formLogin {} - httpBasic {} + formLogin { + failureHandler = CompromisedPasswordAuthenticationFailureHandler() + } } return http.build() } @@ -646,44 +671,22 @@ open fun filterChain(http:HttpSecurity): SecurityFilterChain { open fun compromisedPasswordChecker(): CompromisedPasswordChecker { return HaveIBeenPwnedRestApiPasswordChecker() } ----- -====== - -By doing that, when you try to authenticate via HTTP Basic or Form Login using a weak password, let's say `123456`, you will receive a 401 response status code. -However, just a 401 is not so useful in that case, it will cause some confusion because the user provided the right password and still was not allowed to log in. -In such cases, you can handle the `CompromisedPasswordException` to perform your desired logic, like redirecting the user-agent to `/reset-password`, for example: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@ControllerAdvice -public class MyControllerAdvice { - - @ExceptionHandler(CompromisedPasswordException.class) - public String handleCompromisedPasswordException(CompromisedPasswordException ex, RedirectAttributes attributes) { - attributes.addFlashAttribute("error", ex.message); - return "redirect:/reset-password"; - } - -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@ControllerAdvice -class MyControllerAdvice { - @ExceptionHandler(CompromisedPasswordException::class) - fun handleCompromisedPasswordException(ex: CompromisedPasswordException, attributes: RedirectAttributes): RedirectView { - attributes.addFlashAttribute("error", ex.message) - return RedirectView("/reset-password") +class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler { + private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error") + private val redirectStrategy = DefaultRedirectStrategy() + + override fun onAuthenticationFailure( + request: HttpServletRequest, + response: HttpServletResponse, + exception: AuthenticationException + ) { + if (exception is CompromisedPasswordException) { + redirectStrategy.sendRedirect(request, response, "/reset-password") + return + } + defaultFailureHandler.onAuthenticationFailure(request, response, exception) } - } ---- ======