Improve Compromised Password Sample

Closes gh-287
This commit is contained in:
Marcus Hert Da Coregio
2024-07-31 14:10:40 -03:00
parent 3aca13a0c5
commit d564447444
4 changed files with 46 additions and 79 deletions

View File

@@ -16,14 +16,11 @@
package org.example.compromisedpasswordchecker;
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
import org.springframework.security.authentication.password.CompromisedPasswordDecision;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@@ -33,41 +30,20 @@ class ResetPasswordController {
private final PasswordEncoder passwordEncoder;
private final CompromisedPasswordChecker passwordChecker;
ResetPasswordController(InMemoryUserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder,
CompromisedPasswordChecker passwordChecker) {
ResetPasswordController(InMemoryUserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
this.passwordChecker = passwordChecker;
}
@GetMapping("/reset-password")
String resetPasswordPage() {
return "reset-password";
}
@PostMapping("/reset-password")
String resetPassword(ResetPasswordRequest resetPasswordRequest) {
UserDetails user = this.userDetailsManager.loadUserByUsername(resetPasswordRequest.username());
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
CompromisedPasswordDecision compromisedPassword = this.passwordChecker
.check(resetPasswordRequest.newPassword());
if (compromisedPassword.isCompromised()) {
return "redirect:/reset-password?error=compromised_password";
}
boolean oldPasswordMatches = this.passwordEncoder.matches(resetPasswordRequest.currentPassword(),
user.getPassword());
if (!oldPasswordMatches) {
return "redirect:/reset-password?error=invalid_password";
}
this.userDetailsManager.updatePassword(user, this.passwordEncoder.encode(resetPasswordRequest.newPassword()));
return "redirect:/login";
String resetPassword(ResetPasswordRequest resetPasswordRequest, HttpSession session) {
String newPassword = this.passwordEncoder.encode(resetPasswordRequest.newPassword());
this.userDetailsManager.changePassword(resetPasswordRequest.currentPassword(), newPassword);
session.removeAttribute("compromised_password");
return "redirect:/";
}
record ResetPasswordRequest(String username, String currentPassword, String newPassword) {
record ResetPasswordRequest(String currentPassword, String newPassword) {
}
}

View File

@@ -21,26 +21,29 @@ import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
import org.springframework.security.authentication.password.CompromisedPasswordException;
import org.springframework.security.authentication.password.CompromisedPasswordDecision;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.password.HaveIBeenPwnedRestApiPasswordChecker;
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfig {
@Bean
@@ -52,15 +55,17 @@ public class SecurityConfig {
.anyRequest().authenticated()
)
.formLogin((login) -> login
.failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
.successHandler(new CompromisedPasswordAwareAuthenticationSuccessHandler())
);
// @formatter:on
return http.build();
}
@Bean
CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
@Autowired
void configure(AuthenticationManagerBuilder builder) {
// @formatter:off
builder.eraseCredentials(false); // Do not clear credentials after authentication, so we have access to passwords on success handlers
// @formatter:on
}
@Bean
@@ -78,21 +83,22 @@ public class SecurityConfig {
return new InMemoryUserDetailsManager(user);
}
static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
static class CompromisedPasswordAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
"/login?error");
private final AuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler("/");
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final CompromisedPasswordChecker compromisedPasswordChecker = new HaveIBeenPwnedRestApiPasswordChecker();
@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;
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
CompromisedPasswordDecision decision = this.compromisedPasswordChecker
.check((String) authentication.getCredentials());
if (decision.isCompromised()) {
HttpSession session = request.getSession(false);
session.setAttribute("compromised_password", true);
}
this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
this.successHandler.onAuthenticationSuccess(request, response, authentication);
}
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" lang="en">
<head>
<title>Hello Spring Security</title>
<meta charset="utf-8" />
@@ -7,5 +7,15 @@
<body>
<h1>Hello Spring Security</h1>
<p>This is a secured page</p>
<div th:if="${#ctx.session.compromised_password != null}">
<h3>Your password is compromised, please consider resetting it:</h3>
<form th:action="@{/reset-password}" method="post">
<label for="currentPassword">Current password</label>
<input type="password" id="currentPassword" name="currentPassword" > <br/>
<label for="newPassword">New password</label>
<input type="password" id="newPassword" name="newPassword"> <br/>
<input id="submit" type="submit" value="Submit"/>
</form>
</div>
</body>
</html>

View File

@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" lang="en">
<head>
<title>Reset Password</title>
<meta charset="utf-8"/>
</head>
<body>
<h1>Reset your password</h1>
<div th:if="${param.error != null}" style="color: red">
<h3 th:text="${param.error}">
Error
</h3>
</div>
<form th:action="@{/reset-password}" method="post">
<label for="username">Username</label>:
<input type="text" id="username" name="username" autofocus="autofocus"/> <br/>
<label for="currentPassword">Current password</label>
<input type="password" id="currentPassword" name="currentPassword" > <br/>
<label for="newPassword">New password</label>
<input type="password" id="newPassword" name="newPassword"> <br/>
<input id="submit" type="submit" value="Submit"/>
</form>
<p><a href="/login" th:href="@{/login}">Back to login</a></p>
</body>
</html>