Explorar o código

Support A Well-Known URL for Changing Passwords

Closes gh-8657
Evgeniy Cheban %!s(int64=5) %!d(string=hai) anos
pai
achega
d121ab9565
Modificáronse 25 ficheiros con 1307 adicións e 4 borrados
  1. 4 1
      config/src/main/java/org/springframework/security/config/Elements.java
  2. 40 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
  3. 62 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurer.java
  4. 16 0
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  5. 4 1
      config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
  6. 61 0
      config/src/main/java/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParser.java
  7. 103 0
      config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
  8. 31 1
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt
  9. 36 0
      config/src/main/kotlin/org/springframework/security/config/web/server/ServerPasswordManagementDsl.kt
  10. 29 0
      config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt
  11. 39 0
      config/src/main/kotlin/org/springframework/security/config/web/servlet/PasswordManagementDsl.kt
  12. 9 1
      config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc
  13. 19 0
      config/src/main/resources/org/springframework/security/config/spring-security-5.6.xsd
  14. 116 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurerTests.java
  15. 65 0
      config/src/test/java/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests.java
  16. 79 0
      config/src/test/java/org/springframework/security/config/web/server/PasswordManagementSpecTests.java
  17. 97 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerPasswordManagementDslTests.kt
  18. 84 0
      config/src/test/kotlin/org/springframework/security/config/web/servlet/PasswordManagementDslTests.kt
  19. 32 0
      config/src/test/resources/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests-CustomChangePasswordPage.xml
  20. 32 0
      config/src/test/resources/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests-DefaultChangePasswordPage.xml
  21. 16 0
      docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc
  22. 71 0
      web/src/main/java/org/springframework/security/web/RequestMatcherRedirectFilter.java
  23. 70 0
      web/src/main/java/org/springframework/security/web/server/ExchangeMatcherRedirectWebFilter.java
  24. 105 0
      web/src/test/java/org/springframework/security/web/RequestMatcherRedirectFilterTests.java
  25. 87 0
      web/src/test/java/org/springframework/security/web/server/ExchangeMatcherRedirectWebFilterTests.java

+ 4 - 1
config/src/main/java/org/springframework/security/config/Elements.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ package org.springframework.security.config;
  * Contains all the element names used by Spring Security 3 namespace support.
  *
  * @author Ben Alex
+ * @author Evgeniy Cheban
  */
 public abstract class Elements {
 
@@ -135,4 +136,6 @@ public abstract class Elements {
 
 	public static final String CLIENT_REGISTRATIONS = "client-registrations";
 
+	public static final String PASSWORD_MANAGEMENT = "password-management";
+
 }

+ 40 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -59,6 +59,7 @@ import org.springframework.security.config.annotation.web.configurers.HeadersCon
 import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
 import org.springframework.security.config.annotation.web.configurers.JeeConfigurer;
 import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
+import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer;
 import org.springframework.security.config.annotation.web.configurers.PortMapperConfigurer;
 import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
 import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
@@ -2682,6 +2683,45 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
 		return HttpSecurity.this;
 	}
 
+	/**
+	 * Adds support for the password management.
+	 *
+	 * <h2>Example Configuration</h2> The example below demonstrates how to configure
+	 * password management for an application. The default change password page is
+	 * "/change-password", but can be customized using
+	 * {@link PasswordManagementConfigurer#changePasswordPage(String)}.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class PasswordManagementSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	&#064;Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
+	 * 			)
+	 * 			.passwordManagement(passwordManagement ->
+	 * 				passwordManagement
+	 * 					.changePasswordPage(&quot;/custom-change-password-page&quot;)
+	 * 			);
+	 *  }
+	 * }
+	 * </pre>
+	 * @param passwordManagementCustomizer the {@link Customizer} to provide more options
+	 * for the {@link PasswordManagementConfigurer}
+	 * @return the {@link HttpSecurity} for further customizations
+	 * @throws Exception
+	 * @since 5.6
+	 */
+	public HttpSecurity passwordManagement(
+			Customizer<PasswordManagementConfigurer<HttpSecurity>> passwordManagementCustomizer) throws Exception {
+		passwordManagementCustomizer.customize(getOrApply(new PasswordManagementConfigurer<>()));
+		return HttpSecurity.this;
+	}
+
 	@Override
 	public <C> void setSharedObject(Class<C> sharedType, C object) {
 		super.setSharedObject(sharedType, object);

+ 62 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurer.java

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.web.configurers;
+
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.web.RequestMatcherRedirectFilter;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Adds password management support.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class PasswordManagementConfigurer<B extends HttpSecurityBuilder<B>>
+		extends AbstractHttpConfigurer<PasswordManagementConfigurer<B>, B> {
+
+	private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";
+
+	private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";
+
+	private String changePasswordPage = DEFAULT_CHANGE_PASSWORD_PAGE;
+
+	/**
+	 * Sets the change password page. Defaults to
+	 * {@link PasswordManagementConfigurer#DEFAULT_CHANGE_PASSWORD_PAGE}.
+	 * @param changePasswordPage the change password page
+	 * @return the {@link PasswordManagementConfigurer} for further customizations
+	 */
+	public PasswordManagementConfigurer<B> changePasswordPage(String changePasswordPage) {
+		Assert.hasText(changePasswordPage, "changePasswordPage cannot be empty");
+		this.changePasswordPage = changePasswordPage;
+		return this;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void configure(B http) throws Exception {
+		RequestMatcherRedirectFilter changePasswordFilter = new RequestMatcherRedirectFilter(
+				new AntPathRequestMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage);
+		http.addFilterBefore(postProcess(changePasswordFilter), UsernamePasswordAuthenticationFilter.class);
+	}
+
+}

+ 16 - 0
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -176,6 +176,8 @@ class HttpConfigurationBuilder {
 
 	private BeanDefinition csrfFilter;
 
+	private BeanDefinition wellKnownChangePasswordRedirectFilter;
+
 	private BeanMetadataElement csrfLogoutHandler;
 
 	private BeanMetadataElement csrfAuthStrategy;
@@ -210,6 +212,7 @@ class HttpConfigurationBuilder {
 		createFilterSecurityInterceptor(authenticationManager);
 		createAddHeadersFilter();
 		createCorsFilter();
+		createWellKnownChangePasswordRedirectFilter();
 	}
 
 	private void validateInterceptUrls(ParserContext pc) {
@@ -694,6 +697,15 @@ class HttpConfigurationBuilder {
 		this.csrfLogoutHandler = this.csrfParser.getCsrfLogoutHandler();
 	}
 
+	private void createWellKnownChangePasswordRedirectFilter() {
+		Element element = DomUtils.getChildElementByTagName(this.httpElt, Elements.PASSWORD_MANAGEMENT);
+		if (element == null) {
+			return;
+		}
+		WellKnownChangePasswordBeanDefinitionParser parser = new WellKnownChangePasswordBeanDefinitionParser();
+		this.wellKnownChangePasswordRedirectFilter = parser.parse(element, this.pc);
+	}
+
 	BeanMetadataElement getCsrfLogoutHandler() {
 		return this.csrfLogoutHandler;
 	}
@@ -744,6 +756,10 @@ class HttpConfigurationBuilder {
 		if (this.csrfFilter != null) {
 			filters.add(new OrderDecorator(this.csrfFilter, SecurityFilters.CSRF_FILTER));
 		}
+		if (this.wellKnownChangePasswordRedirectFilter != null) {
+			filters.add(new OrderDecorator(this.wellKnownChangePasswordRedirectFilter,
+					SecurityFilters.WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER));
+		}
 		return filters;
 	}
 

+ 4 - 1
config/src/main/java/org/springframework/security/config/http/SecurityFilters.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ package org.springframework.security.config.http;
  *
  * @author Luke Taylor
  * @author Rob Winch
+ * @author Evgeniy Cheban
  */
 
 enum SecurityFilters {
@@ -80,6 +81,8 @@ enum SecurityFilters {
 
 	OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER,
 
+	WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER,
+
 	SESSION_MANAGEMENT_FILTER,
 
 	EXCEPTION_TRANSLATION_FILTER,

+ 61 - 0
config/src/main/java/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParser.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.http;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.security.web.RequestMatcherRedirectFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.StringUtils;
+
+/**
+ * The bean definition parser for a Well-Known URL for Changing Passwords.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class WellKnownChangePasswordBeanDefinitionParser implements BeanDefinitionParser {
+
+	private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";
+
+	private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";
+
+	private static final String ATT_CHANGE_PASSWORD_PAGE = "change-password-page";
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public BeanDefinition parse(Element element, ParserContext parserContext) {
+		BeanDefinition changePasswordFilter = BeanDefinitionBuilder
+				.rootBeanDefinition(RequestMatcherRedirectFilter.class)
+				.addConstructorArgValue(new AntPathRequestMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN))
+				.addConstructorArgValue(getChangePasswordPage(element)).getBeanDefinition();
+		parserContext.getReaderContext().registerWithGeneratedName(changePasswordFilter);
+		return changePasswordFilter;
+	}
+
+	private String getChangePasswordPage(Element element) {
+		String changePasswordPage = element.getAttribute(ATT_CHANGE_PASSWORD_PAGE);
+		return (StringUtils.hasText(changePasswordPage) ? changePasswordPage : DEFAULT_CHANGE_PASSWORD_PAGE);
+	}
+
+}

+ 103 - 0
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -104,6 +104,7 @@ import org.springframework.security.web.authentication.preauth.x509.SubjectDnX50
 import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
 import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
+import org.springframework.security.web.server.ExchangeMatcherRedirectWebFilter;
 import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
@@ -261,6 +262,8 @@ public class ServerHttpSecurity {
 
 	private HttpBasicSpec httpBasic;
 
+	private PasswordManagementSpec passwordManagement;
+
 	private X509Spec x509;
 
 	private final RequestCacheSpec requestCache = new RequestCacheSpec();
@@ -683,6 +686,56 @@ public class ServerHttpSecurity {
 		return this;
 	}
 
+	/**
+	 * Configures password management. An example configuration is provided below:
+	 *
+	 * <pre class="code">
+	 *  &#064;Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	 *      http
+	 *          // ...
+	 *          .passwordManagement();
+	 *      return http.build();
+	 *  }
+	 * </pre>
+	 * @return the {@link PasswordManagementSpec} to customize
+	 * @since 5.6
+	 */
+	public PasswordManagementSpec passwordManagement() {
+		if (this.passwordManagement == null) {
+			this.passwordManagement = new PasswordManagementSpec();
+		}
+		return this.passwordManagement;
+	}
+
+	/**
+	 * Configures password management. An example configuration is provided below:
+	 *
+	 * <pre class="code">
+	 *  &#064;Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	 *      http
+	 *          // ...
+	 *          .passwordManagement(passwordManagement ->
+	 *          	// Custom change password page.
+	 *          	passwordManagement.changePasswordPage("/custom-change-password-page")
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * </pre>
+	 * @param passwordManagementCustomizer the {@link Customizer} to provide more options
+	 * for the {@link PasswordManagementSpec}
+	 * @return the {@link ServerHttpSecurity} to customize
+	 * @since 5.6
+	 */
+	public ServerHttpSecurity passwordManagement(Customizer<PasswordManagementSpec> passwordManagementCustomizer) {
+		if (this.passwordManagement == null) {
+			this.passwordManagement = new PasswordManagementSpec();
+		}
+		passwordManagementCustomizer.customize(this.passwordManagement);
+		return this;
+	}
+
 	/**
 	 * Configures form based authentication. An example configuration is provided below:
 	 *
@@ -1348,6 +1401,9 @@ public class ServerHttpSecurity {
 			}
 			this.httpBasic.configure(this);
 		}
+		if (this.passwordManagement != null) {
+			this.passwordManagement.configure(this);
+		}
 		if (this.formLogin != null) {
 			if (this.formLogin.authenticationManager == null) {
 				this.formLogin.authenticationManager(this.authenticationManager);
@@ -2018,6 +2074,53 @@ public class ServerHttpSecurity {
 
 	}
 
+	/**
+	 * Configures password management.
+	 *
+	 * @author Evgeniy Cheban
+	 * @since 5.6
+	 * @see #passwordManagement()
+	 */
+	public final class PasswordManagementSpec {
+
+		private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";
+
+		private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";
+
+		private String changePasswordPage = DEFAULT_CHANGE_PASSWORD_PAGE;
+
+		/**
+		 * Sets the change password page. Defaults to
+		 * {@link PasswordManagementSpec#DEFAULT_CHANGE_PASSWORD_PAGE}.
+		 * @param changePasswordPage the change password page
+		 * @return the {@link PasswordManagementSpec} to continue configuring
+		 */
+		public PasswordManagementSpec changePasswordPage(String changePasswordPage) {
+			Assert.hasText(changePasswordPage, "changePasswordPage cannot be empty");
+			this.changePasswordPage = changePasswordPage;
+			return this;
+		}
+
+		/**
+		 * Allows method chaining to continue configuring the {@link ServerHttpSecurity}.
+		 * @return the {@link ServerHttpSecurity} to continue configuring
+		 */
+		public ServerHttpSecurity and() {
+			return ServerHttpSecurity.this;
+		}
+
+		protected void configure(ServerHttpSecurity http) {
+			ExchangeMatcherRedirectWebFilter changePasswordWebFilter = new ExchangeMatcherRedirectWebFilter(
+					new PathPatternParserServerWebExchangeMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN),
+					this.changePasswordPage);
+			http.addFilterBefore(changePasswordWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
+		}
+
+		private PasswordManagementSpec() {
+		}
+
+	}
+
 	/**
 	 * Configures Form Based authentication
 	 *

+ 31 - 1
config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -251,6 +251,36 @@ class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val in
         this.http.httpBasic(httpBasicCustomizer)
     }
 
+    /**
+     * Enables password management.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebFluxSecurity
+     * class SecurityConfig {
+     *
+     *  @Bean
+     *  fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+     *      return http {
+     *          passwordManagement {
+     *              changePasswordPage = "/custom-change-password-page"
+     *          }
+     *       }
+     *   }
+     * }
+     * ```
+     *
+     * @param passwordManagementConfiguration custom configuration to be applied to the
+     * password management
+     * @see [ServerPasswordManagementDsl]
+     * @since 5.6
+     */
+    fun passwordManagement(passwordManagementConfiguration: ServerPasswordManagementDsl.() -> Unit) {
+        val passwordManagementCustomizer = ServerPasswordManagementDsl().apply(passwordManagementConfiguration).get()
+        this.http.passwordManagement(passwordManagementCustomizer)
+    }
+
     /**
      * Allows configuring response headers.
      *

+ 36 - 0
config/src/main/kotlin/org/springframework/security/config/web/server/ServerPasswordManagementDsl.kt

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.server
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] password management
+ * using idiomatic Kotlin code.
+ *
+ * @author Evgeniy Cheban
+ * @property changePasswordPage the change password page.
+ * @since 5.6
+ */
+@ServerSecurityMarker
+class ServerPasswordManagementDsl {
+    var changePasswordPage: String? = null
+
+    internal fun get(): (ServerHttpSecurity.PasswordManagementSpec) -> Unit {
+        return { passwordManagement ->
+            changePasswordPage?.also { passwordManagement.changePasswordPage(changePasswordPage) }
+        }
+    }
+}

+ 29 - 0
config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt

@@ -222,6 +222,35 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
         this.http.httpBasic(httpBasicCustomizer)
     }
 
+    /**
+     * Enables password management.
+     *
+     * Example:
+     *
+     * ```
+     * @EnableWebSecurity
+     * class SecurityConfig : WebSecurityConfigurerAdapter() {
+     *
+     *  override fun configure(http: HttpSecurity) {
+     *      http {
+     *          passwordManagement {
+     *              changePasswordPage = "/custom-change-password-page"
+     *          }
+     *      }
+     *  }
+     * }
+     * ```
+     *
+     * @param passwordManagementConfiguration custom configurations to be applied to the
+     * password management
+     * @see [PasswordManagementDsl]
+     * @since 5.6
+     */
+    fun passwordManagement(passwordManagementConfiguration: PasswordManagementDsl.() -> Unit) {
+        val passwordManagementCustomizer = PasswordManagementDsl().apply(passwordManagementConfiguration).get()
+        this.http.passwordManagement(passwordManagementCustomizer)
+    }
+
     /**
      * Allows configuring response headers.
      *

+ 39 - 0
config/src/main/kotlin/org/springframework/security/config/web/servlet/PasswordManagementDsl.kt

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.servlet
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer
+
+/**
+ * A Kotlin DSL to configure [HttpSecurity] password management
+ * using idiomatic Kotlin code.
+ *
+ * @author Evgeniy Cheban
+ * @property changePasswordPage the change password page.
+ * @since 5.6
+ */
+@SecurityMarker
+class PasswordManagementDsl {
+    var changePasswordPage: String? = null
+
+    internal fun get(): (PasswordManagementConfigurer<HttpSecurity>) -> Unit {
+        return { passwordManagement ->
+            changePasswordPage?.also { passwordManagement.changePasswordPage(changePasswordPage) }
+        }
+    }
+}

+ 9 - 1
config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc

@@ -312,7 +312,7 @@ http-firewall =
 
 http =
 	## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none".
-	element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) }
+	element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) }
 http.attlist &=
 	## The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.
 	attribute pattern {xsd:token}?
@@ -703,6 +703,14 @@ http-basic.attlist &=
 	## Reference to an AuthenticationDetailsSource which will be used by the authentication filter
 	attribute authentication-details-source-ref {xsd:token}?
 
+password-management =
+    ## Adds support for the password management.
+    element password-management {password-management.attlist, empty}
+
+password-management.attlist &=
+    ## The change password page. Defaults to "/change-password".
+    attribute change-password-page {xsd:string}?
+
 session-management =
 	## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack.
 	element session-management {session-management.attlist, concurrency-control?}

+ 19 - 0
config/src/main/resources/org/springframework/security/config/spring-security-5.6.xsd

@@ -1066,6 +1066,7 @@
                   <xs:attributeGroup ref="security:logout.attlist"/>
                </xs:complexType>
             </xs:element>
+			<xs:element ref="security:password-management"/>
             <xs:element name="session-management">
                <xs:annotation>
                   <xs:documentation>Session-management related functionality is implemented by the addition of a
@@ -2161,6 +2162,23 @@
       </xs:attribute>
   </xs:attributeGroup>
   
+  <xs:element name="password-management">
+	  <xs:annotation>
+		 <xs:documentation>Adds support for the password management.
+		 		</xs:documentation>
+	  </xs:annotation>
+	  <xs:complexType>
+		 <xs:attributeGroup ref="security:password-management.attlist"/>
+	  </xs:complexType>
+  </xs:element>
+  <xs:attributeGroup name="password-management.attlist">
+      <xs:attribute name="change-password-page" type="xs:string">
+         <xs:annotation>
+            <xs:documentation>The change password page. Defaults to "/change-password".
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+  </xs:attributeGroup>
   <xs:attributeGroup name="session-management.attlist">
       <xs:attribute name="session-fixation-protection">
          <xs:annotation>
@@ -3255,6 +3273,7 @@
          <xs:enumeration value="REMEMBER_ME_FILTER"/>
          <xs:enumeration value="ANONYMOUS_FILTER"/>
          <xs:enumeration value="OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER"/>
+         <xs:enumeration value="WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER"/>
          <xs:enumeration value="SESSION_MANAGEMENT_FILTER"/>
          <xs:enumeration value="EXCEPTION_TRANSLATION_FILTER"/>
          <xs:enumeration value="FILTER_SECURITY_INTERCEPTOR"/>

+ 116 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurerTests.java

@@ -0,0 +1,116 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.annotation.web.configurers;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.springframework.security.config.Customizer.withDefaults;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link PasswordManagementConfigurer}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PasswordManagementConfigurerTests {
+
+	@Rule
+	public final SpringTestRule spring = new SpringTestRule();
+
+	@Autowired
+	MockMvc mvc;
+
+	@Test
+	public void whenChangePasswordPageNotSetThenDefaultChangePasswordPageUsed() throws Exception {
+		this.spring.register(PasswordManagementWithDefaultChangePasswordPageConfig.class).autowire();
+
+		this.mvc.perform(get("/.well-known/change-password")).andExpect(status().isFound())
+				.andExpect(redirectedUrl("/change-password"));
+	}
+
+	@Test
+	public void whenChangePasswordPageSetThenSpecifiedChangePasswordPageUsed() throws Exception {
+		this.spring.register(PasswordManagementWithCustomChangePasswordPageConfig.class).autowire();
+
+		this.mvc.perform(get("/.well-known/change-password")).andExpect(status().isFound())
+				.andExpect(redirectedUrl("/custom-change-password-page"));
+	}
+
+	@Test
+	public void whenSettingNullChangePasswordPage() {
+		PasswordManagementConfigurer configurer = new PasswordManagementConfigurer();
+		assertThatIllegalArgumentException().isThrownBy(() -> configurer.changePasswordPage(null))
+				.withMessage("changePasswordPage cannot be empty");
+	}
+
+	@Test
+	public void whenSettingEmptyChangePasswordPage() {
+		PasswordManagementConfigurer configurer = new PasswordManagementConfigurer();
+		assertThatIllegalArgumentException().isThrownBy(() -> configurer.changePasswordPage(""))
+				.withMessage("changePasswordPage cannot be empty");
+	}
+
+	@Test
+	public void whenSettingBlankChangePasswordPage() {
+		PasswordManagementConfigurer configurer = new PasswordManagementConfigurer();
+		assertThatIllegalArgumentException().isThrownBy(() -> configurer.changePasswordPage(" "))
+				.withMessage("changePasswordPage cannot be empty");
+	}
+
+	@EnableWebSecurity
+	static class PasswordManagementWithDefaultChangePasswordPageConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			return http
+					.passwordManagement(withDefaults())
+					.build();
+			// @formatter:on
+		}
+
+	}
+
+	@EnableWebSecurity
+	static class PasswordManagementWithCustomChangePasswordPageConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			return http
+					.passwordManagement((passwordManagement) -> passwordManagement
+						.changePasswordPage("/custom-change-password-page")
+					)
+					.build();
+			// @formatter:on
+		}
+
+	}
+
+}

+ 65 - 0
config/src/test/java/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.http;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link WellKnownChangePasswordBeanDefinitionParser}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class WellKnownChangePasswordBeanDefinitionParserTests {
+
+	private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests";
+
+	@Rule
+	public final SpringTestRule spring = new SpringTestRule();
+
+	@Autowired
+	MockMvc mvc;
+
+	@Test
+	public void whenChangePasswordPageNotSetThenDefaultChangePasswordPageUsed() throws Exception {
+		this.spring.configLocations(xml("DefaultChangePasswordPage")).autowire();
+
+		this.mvc.perform(get("/.well-known/change-password")).andExpect(status().isFound())
+				.andExpect(redirectedUrl("/change-password"));
+	}
+
+	@Test
+	public void whenChangePasswordPageSetThenSpecifiedChangePasswordPageUsed() throws Exception {
+		this.spring.configLocations(xml("CustomChangePasswordPage")).autowire();
+
+		this.mvc.perform(get("/.well-known/change-password")).andExpect(status().isFound())
+				.andExpect(redirectedUrl("/custom-change-password-page"));
+	}
+
+	private String xml(String configName) {
+		return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+	}
+
+}

+ 79 - 0
config/src/test/java/org/springframework/security/config/web/server/PasswordManagementSpecTests.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.server;
+
+import org.apache.http.HttpHeaders;
+import org.junit.Test;
+
+import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder;
+import org.springframework.security.config.web.server.ServerHttpSecurity.PasswordManagementSpec;
+import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link PasswordManagementSpec}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class PasswordManagementSpecTests {
+
+	ServerHttpSecurity http = ServerHttpSecurityConfigurationBuilder.httpWithDefaultAuthentication();
+
+	@Test
+	public void whenChangePasswordPageNotSetThenDefaultChangePasswordPageUsed() {
+		this.http.passwordManagement();
+
+		WebTestClient client = buildClient();
+		client.get().uri("/.well-known/change-password").exchange().expectStatus().isFound().expectHeader()
+				.valueEquals(HttpHeaders.LOCATION, "/change-password");
+	}
+
+	@Test
+	public void whenChangePasswordPageSetThenSpecifiedChangePasswordPageUsed() {
+		this.http.passwordManagement(
+				(passwordManagement) -> passwordManagement.changePasswordPage("/custom-change-password-page"));
+
+		WebTestClient client = buildClient();
+		client.get().uri("/.well-known/change-password").exchange().expectStatus().isFound().expectHeader()
+				.valueEquals(HttpHeaders.LOCATION, "/custom-change-password-page");
+	}
+
+	private WebTestClient buildClient() {
+		return WebTestClientBuilder.bindToWebFilters(this.http.build()).build();
+	}
+
+	@Test
+	public void whenSettingNullChangePasswordPage() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.http.passwordManagement().changePasswordPage(null))
+				.withMessage("changePasswordPage cannot be empty");
+	}
+
+	@Test
+	public void whenSettingEmptyChangePasswordPage() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.http.passwordManagement().changePasswordPage(""))
+				.withMessage("changePasswordPage cannot be empty");
+	}
+
+	@Test
+	public void whenSettingBlankChangePasswordPage() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.http.passwordManagement().changePasswordPage(" "))
+				.withMessage("changePasswordPage cannot be empty");
+	}
+
+}

+ 97 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerPasswordManagementDslTests.kt

@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.server
+
+import org.apache.http.HttpHeaders
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.security.web.server.SecurityWebFilterChain
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.web.reactive.config.EnableWebFlux
+
+/**
+ * Tests for [ServerPasswordManagementDsl].
+ *
+ * @author Evgeniy Cheban
+ */
+class ServerPasswordManagementDslTests {
+
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    private lateinit var client: WebTestClient
+
+    @Autowired
+    fun setup(context: ApplicationContext) {
+        this.client = WebTestClient
+                .bindToApplicationContext(context)
+                .configureClient()
+                .build()
+    }
+
+    @Test
+    fun `when change password page not set then default change password page used`() {
+        this.spring.register(PasswordManagementWithDefaultChangePasswordPageConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/.well-known/change-password")
+                .exchange()
+                .expectStatus().isFound
+                .expectHeader().valueEquals(HttpHeaders.LOCATION, "/change-password")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PasswordManagementWithDefaultChangePasswordPageConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                passwordManagement {}
+            }
+        }
+    }
+
+    @Test
+    fun `when change password page set then specified change password page used`() {
+        this.spring.register(PasswordManagementWithCustomChangePasswordPageConfig::class.java).autowire()
+
+        this.client.get()
+                .uri("/.well-known/change-password")
+                .exchange()
+                .expectStatus().isFound
+                .expectHeader().valueEquals(HttpHeaders.LOCATION, "/custom-change-password-page")
+    }
+
+    @EnableWebFluxSecurity
+    @EnableWebFlux
+    open class PasswordManagementWithCustomChangePasswordPageConfig {
+        @Bean
+        open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
+            return http {
+                passwordManagement {
+                    changePasswordPage = "/custom-change-password-page"
+                }
+            }
+        }
+    }
+}

+ 84 - 0
config/src/test/kotlin/org/springframework/security/config/web/servlet/PasswordManagementDslTests.kt

@@ -0,0 +1,84 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.config.web.servlet
+
+import org.junit.Rule
+import org.junit.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
+import org.springframework.security.config.test.SpringTestRule
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+
+/**
+ * Tests for [PasswordManagementDsl].
+ *
+ * @author Evgeniy Cheban
+ */
+class PasswordManagementDslTests {
+
+    @Rule
+    @JvmField
+    val spring = SpringTestRule()
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `when change password page not set then default change password page used`() {
+        this.spring.register(PasswordManagementWithDefaultChangePasswordPageConfig::class.java).autowire()
+
+        this.mockMvc.get("/.well-known/change-password")
+                .andExpect {
+                    status { isFound() }
+                    redirectedUrl("/change-password")
+                }
+    }
+
+    @EnableWebSecurity
+    open class PasswordManagementWithDefaultChangePasswordPageConfig : WebSecurityConfigurerAdapter() {
+        override fun configure(http: HttpSecurity) {
+            http {
+                passwordManagement {}
+            }
+        }
+    }
+
+    @Test
+    fun `when change password page set then specified change password page used`() {
+        this.spring.register(PasswordManagementWithCustomChangePasswordPageConfig::class.java).autowire()
+
+        this.mockMvc.get("/.well-known/change-password")
+                .andExpect {
+                    status { isFound() }
+                    redirectedUrl("/custom-change-password-page")
+                }
+    }
+
+    @EnableWebSecurity
+    open class PasswordManagementWithCustomChangePasswordPageConfig : WebSecurityConfigurerAdapter() {
+        override fun configure(http: HttpSecurity) {
+            http {
+                passwordManagement {
+                    changePasswordPage = "/custom-change-password-page"
+                }
+            }
+        }
+    }
+}

+ 32 - 0
config/src/test/resources/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests-CustomChangePasswordPage.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2021 the original author or authors.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<password-management change-password-page="/custom-change-password-page"/>
+	</http>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 32 - 0
config/src/test/resources/org/springframework/security/config/http/WellKnownChangePasswordBeanDefinitionParserTests-DefaultChangePasswordPage.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2021 the original author or authors.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<password-management/>
+	</http>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 16 - 0
docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc

@@ -168,6 +168,7 @@ The default value is true.
 * <<nsa-oauth2-login,oauth2-login>>
 * <<nsa-oauth2-resource-server,oauth2-resource-server>>
 * <<nsa-openid-login,openid-login>>
+* <<nsa-password-management,password-management>>
 * <<nsa-port-mappings,port-mappings>>
 * <<nsa-remember-me,remember-me>>
 * <<nsa-request-cache,request-cache>>
@@ -1593,6 +1594,21 @@ Specifies the attribute type.
 For example, https://axschema.org/contact/email.
 See your OP's documentation for valid attribute types.
 
+[[nsa-password-management]]
+==== <password-management>
+This element configures password management.
+
+[[nsa-password-management-parents]]
+===== Parent Elements of <password-management>
+
+* <<nsa-http,http>>
+
+[[nsa-password-management-attributes]]
+===== <password-management> Attributes
+
+[[nsa-password-management-change-password-page]]
+* **change-password-page**
+The change password page. Defaults to "/change-password".
 
 [[nsa-port-mappings]]
 ==== <port-mappings>

+ 71 - 0
web/src/main/java/org/springframework/security/web/RequestMatcherRedirectFilter.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * Filter that redirects requests that match {@link RequestMatcher} to the specified URL.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class RequestMatcherRedirectFilter extends OncePerRequestFilter {
+
+	private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+	private final RequestMatcher requestMatcher;
+
+	private final String redirectUrl;
+
+	/**
+	 * Create and initialize an instance of the filter.
+	 * @param requestMatcher the request matcher
+	 * @param redirectUrl the redirect URL
+	 */
+	public RequestMatcherRedirectFilter(RequestMatcher requestMatcher, String redirectUrl) {
+		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+		Assert.hasText(redirectUrl, "redirectUrl cannot be empty");
+		this.requestMatcher = requestMatcher;
+		this.redirectUrl = redirectUrl;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+			throws ServletException, IOException {
+
+		if (this.requestMatcher.matches(request)) {
+			this.redirectStrategy.sendRedirect(request, response, this.redirectUrl);
+		}
+		else {
+			filterChain.doFilter(request, response);
+		}
+	}
+
+}

+ 70 - 0
web/src/main/java/org/springframework/security/web/server/ExchangeMatcherRedirectWebFilter.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server;
+
+import java.net.URI;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+
+/**
+ * Web filter that redirects requests that match {@link ServerWebExchangeMatcher} to the
+ * specified URL.
+ *
+ * @author Evgeniy Cheban
+ * @since 5.6
+ */
+public final class ExchangeMatcherRedirectWebFilter implements WebFilter {
+
+	private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
+
+	private final ServerWebExchangeMatcher exchangeMatcher;
+
+	private final URI redirectUri;
+
+	/**
+	 * Create and initialize an instance of the web filter.
+	 * @param exchangeMatcher the exchange matcher
+	 * @param redirectUrl the redirect URL
+	 */
+	public ExchangeMatcherRedirectWebFilter(ServerWebExchangeMatcher exchangeMatcher, String redirectUrl) {
+		Assert.notNull(exchangeMatcher, "exchangeMatcher cannot be null");
+		Assert.hasText(redirectUrl, "redirectUrl cannot be empty");
+		this.exchangeMatcher = exchangeMatcher;
+		this.redirectUri = URI.create(redirectUrl);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+		// @formatter:off
+		return this.exchangeMatcher.matches(exchange)
+				.filter(MatchResult::isMatch)
+				.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
+				.flatMap((result) -> this.redirectStrategy.sendRedirect(exchange, this.redirectUri));
+		// @formatter:on
+	}
+
+}

+ 105 - 0
web/src/test/java/org/springframework/security/web/RequestMatcherRedirectFilterTests.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web;
+
+import javax.servlet.FilterChain;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+/**
+ * Tests for {@link RequestMatcherRedirectFilter}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class RequestMatcherRedirectFilterTests {
+
+	@Test
+	public void doFilterWhenRequestMatchThenRedirectToSpecifiedUrl() throws Exception {
+		RequestMatcherRedirectFilter filter = new RequestMatcherRedirectFilter(new AntPathRequestMatcher("/context"),
+				"/test");
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setServletPath("/context");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		filter.doFilter(request, response, filterChain);
+
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
+		assertThat(response.getRedirectedUrl()).isEqualTo("/test");
+
+		verifyNoInteractions(filterChain);
+	}
+
+	@Test
+	public void doFilterWhenRequestNotMatchThenNextFilter() throws Exception {
+		RequestMatcherRedirectFilter filter = new RequestMatcherRedirectFilter(new AntPathRequestMatcher("/context"),
+				"/test");
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setServletPath("/test");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		filter.doFilter(request, response, filterChain);
+
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+
+		verify(filterChain).doFilter(request, response);
+	}
+
+	@Test
+	public void constructWhenRequestMatcherNull() {
+		assertThatIllegalArgumentException().isThrownBy(() -> new RequestMatcherRedirectFilter(null, "/test"))
+				.withMessage("requestMatcher cannot be null");
+	}
+
+	@Test
+	public void constructWhenRedirectUrlNull() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new RequestMatcherRedirectFilter(new AntPathRequestMatcher("/**"), null))
+				.withMessage("redirectUrl cannot be empty");
+	}
+
+	@Test
+	public void constructWhenRedirectUrlEmpty() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new RequestMatcherRedirectFilter(new AntPathRequestMatcher("/**"), ""))
+				.withMessage("redirectUrl cannot be empty");
+	}
+
+	@Test
+	public void constructWhenRedirectUrlBlank() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new RequestMatcherRedirectFilter(new AntPathRequestMatcher("/**"), " "))
+				.withMessage("redirectUrl cannot be empty");
+	}
+
+}

+ 87 - 0
web/src/test/java/org/springframework/security/web/server/ExchangeMatcherRedirectWebFilterTests.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.server.handler.FilteringWebHandler;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link ExchangeMatcherRedirectWebFilter}.
+ *
+ * @author Evgeniy Cheban
+ */
+public class ExchangeMatcherRedirectWebFilterTests {
+
+	@Test
+	public void filterWhenRequestMatchThenRedirectToSpecifiedUrl() {
+		ExchangeMatcherRedirectWebFilter filter = new ExchangeMatcherRedirectWebFilter(
+				new PathPatternParserServerWebExchangeMatcher("/context"), "/test");
+		FilteringWebHandler handler = new FilteringWebHandler((e) -> e.getResponse().setComplete(),
+				Collections.singletonList(filter));
+
+		WebTestClient client = WebTestClient.bindToWebHandler(handler).build();
+		client.get().uri("/context").exchange().expectStatus().isFound().expectHeader()
+				.valueEquals(HttpHeaders.LOCATION, "/test");
+	}
+
+	@Test
+	public void filterWhenRequestNotMatchThenNextFilter() {
+		ExchangeMatcherRedirectWebFilter filter = new ExchangeMatcherRedirectWebFilter(
+				new PathPatternParserServerWebExchangeMatcher("/context"), "/test");
+		FilteringWebHandler handler = new FilteringWebHandler((e) -> e.getResponse().setComplete(),
+				Collections.singletonList(filter));
+
+		WebTestClient client = WebTestClient.bindToWebHandler(handler).build();
+		client.get().uri("/test").exchange().expectStatus().isOk();
+	}
+
+	@Test
+	public void constructWhenExchangeMatcherNull() {
+		assertThatIllegalArgumentException().isThrownBy(() -> new ExchangeMatcherRedirectWebFilter(null, "/test"))
+				.withMessage("exchangeMatcher cannot be null");
+	}
+
+	@Test
+	public void constructWhenRedirectUrlNull() {
+		assertThatIllegalArgumentException().isThrownBy(
+				() -> new ExchangeMatcherRedirectWebFilter(new PathPatternParserServerWebExchangeMatcher("/**"), null))
+				.withMessage("redirectUrl cannot be empty");
+	}
+
+	@Test
+	public void constructWhenRedirectUrlEmpty() {
+		assertThatIllegalArgumentException().isThrownBy(
+				() -> new ExchangeMatcherRedirectWebFilter(new PathPatternParserServerWebExchangeMatcher("/**"), ""))
+				.withMessage("redirectUrl cannot be empty");
+	}
+
+	@Test
+	public void constructWhenRedirectUrlBlank() {
+		assertThatIllegalArgumentException().isThrownBy(
+				() -> new ExchangeMatcherRedirectWebFilter(new PathPatternParserServerWebExchangeMatcher("/**"), " "))
+				.withMessage("redirectUrl cannot be empty");
+	}
+
+}