Improve Compromised Password Sample
Closes gh-287
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user