Эх сурвалжийг харах

Improve Compromised Password Sample

Closes gh-287
Marcus Hert Da Coregio 1 жил өмнө
parent
commit
d564447444

+ 9 - 33
servlet/spring-boot/java/authentication/username-password/compromised-password-checker/src/main/java/org/example/compromisedpasswordchecker/ResetPasswordController.java

@@ -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) {
 	}
 
 }

+ 26 - 20
servlet/spring-boot/java/authentication/username-password/compromised-password-checker/src/main/java/org/example/compromisedpasswordchecker/SecurityConfig.java

@@ -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);
 		}
 
 	}

+ 11 - 1
servlet/spring-boot/java/authentication/username-password/compromised-password-checker/src/main/resources/templates/index.html

@@ -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>

+ 0 - 25
servlet/spring-boot/java/authentication/username-password/compromised-password-checker/src/main/resources/templates/reset-password.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>