Skip to content

Commit 9f75c62

Browse files
committed
Support A Well-Known URL for Changing Passwords
Closes gh-8657
1 parent 44ec061 commit 9f75c62

File tree

37 files changed

+1692
-2
lines changed

37 files changed

+1692
-2
lines changed

config/src/main/java/org/springframework/security/config/Elements.java

+2
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,6 @@ public abstract class Elements {
8080
public static final String OAUTH2_LOGIN = "oauth2-login";
8181
public static final String OAUTH2_CLIENT = "oauth2-client";
8282
public static final String CLIENT_REGISTRATIONS = "client-registrations";
83+
84+
public static final String PASSWORD_MANAGEMENT = "password-management";
8385
}

config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,6 +54,7 @@
5454
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
5555
import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
5656
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
57+
import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer;
5758
import org.springframework.security.core.Authentication;
5859
import org.springframework.security.core.context.SecurityContext;
5960
import org.springframework.security.core.context.SecurityContextHolder;
@@ -2526,6 +2527,46 @@ public HttpSecurity httpBasic(Customizer<HttpBasicConfigurer<HttpSecurity>> http
25262527
return HttpSecurity.this;
25272528
}
25282529

2530+
/**
2531+
* Adds support for the password management.
2532+
*
2533+
* <h2>Example Configuration</h2>
2534+
* The example below demonstrates how to configure password management for an
2535+
* application. The default change password page is "/change-password", but can be
2536+
* customized using {@link PasswordManagementConfigurer#changePasswordPage(String)}.
2537+
*
2538+
* <pre>
2539+
* &#064;Configuration
2540+
* &#064;EnableWebSecurity
2541+
* public class PasswordManagementSecurityConfig extends WebSecurityConfigurerAdapter {
2542+
*
2543+
* &#064;Override
2544+
* protected void configure(HttpSecurity http) throws Exception {
2545+
* http
2546+
* .authorizeRequests(authorizeRequests ->
2547+
* authorizeRequests
2548+
* .antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
2549+
* )
2550+
* .passwordManagement(passwordManagement ->
2551+
* passwordManagement
2552+
* .changePasswordPage(&quot;/custom-change-password-page&quot;)
2553+
* );
2554+
* }
2555+
* }
2556+
* </pre>
2557+
*
2558+
* @param passwordManagementCustomizer the {@link Customizer} to provide more options for
2559+
* the {@link PasswordManagementConfigurer}
2560+
* @return the {@link HttpSecurity} for further customizations
2561+
* @throws Exception
2562+
* @since 5.4
2563+
*/
2564+
public HttpSecurity passwordManagement(Customizer<PasswordManagementConfigurer<HttpSecurity>> passwordManagementCustomizer)
2565+
throws Exception {
2566+
passwordManagementCustomizer.customize(getOrApply(new PasswordManagementConfigurer<>()));
2567+
return HttpSecurity.this;
2568+
}
2569+
25292570
public <C> void setSharedObject(Class<C> sharedType, C object) {
25302571
super.setSharedObject(sharedType, object);
25312572
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.config.annotation.web.configurers;
17+
18+
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
19+
import org.springframework.security.web.RequestMatcherRedirectFilter;
20+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
21+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Adds password management support.
26+
*
27+
* @author Evgeniy Cheban
28+
* @since 5.4
29+
*/
30+
public class PasswordManagementConfigurer<B extends HttpSecurityBuilder<B>> extends
31+
AbstractHttpConfigurer<PasswordManagementConfigurer<B>, B> {
32+
33+
private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";
34+
private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";
35+
36+
private String changePasswordPage = DEFAULT_CHANGE_PASSWORD_PAGE;
37+
38+
/**
39+
* Sets the change password page.
40+
* Defaults to {@link PasswordManagementConfigurer#DEFAULT_CHANGE_PASSWORD_PAGE}.
41+
*
42+
* @param changePasswordPage the change password page
43+
* @return the {@link PasswordManagementConfigurer} for further customizations
44+
*/
45+
public PasswordManagementConfigurer<B> changePasswordPage(String changePasswordPage) {
46+
Assert.hasText(changePasswordPage, "changePasswordPage cannot be empty");
47+
this.changePasswordPage = changePasswordPage;
48+
return this;
49+
}
50+
51+
/**
52+
* {@inheritDoc}
53+
*/
54+
@Override
55+
public void configure(B http) throws Exception {
56+
RequestMatcherRedirectFilter changePasswordFilter = new RequestMatcherRedirectFilter(
57+
new AntPathRequestMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage);
58+
http.addFilterBefore(postProcess(changePasswordFilter), UsernamePasswordAuthenticationFilter.class);
59+
}
60+
}

config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

+18
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import static org.springframework.security.config.http.SecurityFilters.SERVLET_API_SUPPORT_FILTER;
9292
import static org.springframework.security.config.http.SecurityFilters.SESSION_MANAGEMENT_FILTER;
9393
import static org.springframework.security.config.http.SecurityFilters.WEB_ASYNC_MANAGER_FILTER;
94+
import static org.springframework.security.config.http.SecurityFilters.WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER;
9495

9596
/**
9697
* Stateful class which helps HttpSecurityBDP to create the configuration for the
@@ -144,6 +145,7 @@ class HttpConfigurationBuilder {
144145
private BeanDefinition addHeadersFilter;
145146
private BeanMetadataElement corsFilter;
146147
private BeanDefinition csrfFilter;
148+
private BeanDefinition wellKnownChangePasswordRedirectFilter;
147149
private BeanMetadataElement csrfLogoutHandler;
148150
private BeanMetadataElement csrfAuthStrategy;
149151

@@ -193,6 +195,7 @@ class HttpConfigurationBuilder {
193195
createFilterSecurityInterceptor(authenticationManager);
194196
createAddHeadersFilter();
195197
createCorsFilter();
198+
createWellKnownChangePasswordRedirectFilter();
196199
}
197200

198201
private SessionCreationPolicy createPolicy(String createSession) {
@@ -803,6 +806,16 @@ private void createCsrfFilter() {
803806
this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler();
804807
}
805808

809+
private void createWellKnownChangePasswordRedirectFilter() {
810+
Element element = DomUtils.getChildElementByTagName(this.httpElt, Elements.PASSWORD_MANAGEMENT);
811+
if (element == null) {
812+
return;
813+
}
814+
815+
WellKnownChangePasswordBeanDefinitionParser parser = new WellKnownChangePasswordBeanDefinitionParser();
816+
this.wellKnownChangePasswordRedirectFilter = parser.parse(element, this.pc);
817+
}
818+
806819
BeanMetadataElement getCsrfLogoutHandler() {
807820
return this.csrfLogoutHandler;
808821
}
@@ -869,6 +882,11 @@ List<OrderDecorator> getFilters() {
869882
filters.add(new OrderDecorator(csrfFilter, CSRF_FILTER));
870883
}
871884

885+
if (this.wellKnownChangePasswordRedirectFilter != null) {
886+
filters.add(new OrderDecorator(this.wellKnownChangePasswordRedirectFilter,
887+
WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER));
888+
}
889+
872890
return filters;
873891
}
874892

config/src/main/java/org/springframework/security/config/http/SecurityFilters.java

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ enum SecurityFilters {
3333
WEB_ASYNC_MANAGER_FILTER /** {@link WebAsyncManagerIntegrationFilter} */,
3434
HEADERS_FILTER, CORS_FILTER,
3535
CSRF_FILTER,
36+
WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER,
3637
LOGOUT_FILTER,
3738
OAUTH2_AUTHORIZATION_REQUEST_FILTER,
3839
X509_FILTER,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.config.http;
17+
18+
import org.springframework.beans.factory.config.BeanDefinition;
19+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
20+
import org.springframework.beans.factory.xml.BeanDefinitionParser;
21+
import org.springframework.beans.factory.xml.ParserContext;
22+
import org.springframework.security.web.RequestMatcherRedirectFilter;
23+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
24+
import org.springframework.util.StringUtils;
25+
import org.w3c.dom.Element;
26+
27+
/**
28+
* The bean definition parser for a Well-Known URL for Changing Passwords.
29+
*
30+
* @author Evgeniy Cheban
31+
* @since 5.4
32+
*/
33+
public class WellKnownChangePasswordBeanDefinitionParser implements BeanDefinitionParser {
34+
private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";
35+
private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";
36+
private static final String ATT_CHANGE_PASSWORD_PAGE = "change-password-page";
37+
38+
/**
39+
* {@inheritDoc}
40+
*/
41+
@Override
42+
public BeanDefinition parse(Element element, ParserContext parserContext) {
43+
BeanDefinition changePasswordFilter = BeanDefinitionBuilder
44+
.rootBeanDefinition(RequestMatcherRedirectFilter.class)
45+
.addConstructorArgValue(new AntPathRequestMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN))
46+
.addConstructorArgValue(getChangePasswordPage(element))
47+
.getBeanDefinition();
48+
49+
parserContext.getReaderContext().registerWithGeneratedName(changePasswordFilter);
50+
51+
return changePasswordFilter;
52+
}
53+
54+
private String getChangePasswordPage(Element element) {
55+
String changePasswordPage = element.getAttribute(ATT_CHANGE_PASSWORD_PAGE);
56+
return (StringUtils.hasText(changePasswordPage) ? changePasswordPage : DEFAULT_CHANGE_PASSWORD_PAGE);
57+
}
58+
}

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+103
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter;
154154
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter;
155155
import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter;
156+
import org.springframework.security.web.server.ExchangeMatcherRedirectWebFilter;
156157
import org.springframework.security.web.server.savedrequest.NoOpServerRequestCache;
157158
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
158159
import org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter;
@@ -258,6 +259,8 @@ public class ServerHttpSecurity {
258259

259260
private HttpBasicSpec httpBasic;
260261

262+
private PasswordManagementSpec passwordManagement;
263+
261264
private X509Spec x509;
262265

263266
private final RequestCacheSpec requestCache = new RequestCacheSpec();
@@ -734,6 +737,58 @@ public ServerHttpSecurity httpBasic(Customizer<HttpBasicSpec> httpBasicCustomize
734737
return this;
735738
}
736739

740+
/**
741+
* Configures password management. An example configuration is provided below:
742+
*
743+
* <pre class="code">
744+
* &#064;Bean
745+
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
746+
* http
747+
* // ...
748+
* .passwordManagement();
749+
* return http.build();
750+
* }
751+
* </pre>
752+
*
753+
* @return the {@link PasswordManagementSpec} to customize
754+
* @since 5.4
755+
*/
756+
public PasswordManagementSpec passwordManagement() {
757+
if (this.passwordManagement == null) {
758+
this.passwordManagement = new PasswordManagementSpec();
759+
}
760+
return this.passwordManagement;
761+
}
762+
763+
/**
764+
* Configures password management. An example configuration is provided below:
765+
*
766+
* <pre class="code">
767+
* &#064;Bean
768+
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
769+
* http
770+
* // ...
771+
* .passwordManagement(passwordManagement ->
772+
* // Custom change password page.
773+
* passwordManagement.changePasswordPage("/custom-change-password-page")
774+
* );
775+
* return http.build();
776+
* }
777+
* </pre>
778+
*
779+
* @param passwordManagementCustomizer the {@link Customizer} to provide more options for
780+
* the {@link PasswordManagementSpec}
781+
* @return the {@link ServerHttpSecurity} to customize
782+
* @since 5.4
783+
*/
784+
public ServerHttpSecurity passwordManagement(Customizer<PasswordManagementSpec> passwordManagementCustomizer) {
785+
if (this.passwordManagement == null) {
786+
this.passwordManagement = new PasswordManagementSpec();
787+
}
788+
passwordManagementCustomizer.customize(this.passwordManagement);
789+
return this;
790+
}
791+
737792
/**
738793
* Configures form based authentication. An example configuration is provided below:
739794
*
@@ -2448,6 +2503,9 @@ else if (this.securityContextRepository != null) {
24482503
}
24492504
this.httpBasic.configure(this);
24502505
}
2506+
if (this.passwordManagement != null) {
2507+
this.passwordManagement.configure(this);
2508+
}
24512509
if (this.formLogin != null) {
24522510
if (this.formLogin.authenticationManager == null) {
24532511
this.formLogin.authenticationManager(this.authenticationManager);
@@ -3054,6 +3112,51 @@ protected void configure(ServerHttpSecurity http) {
30543112
private HttpBasicSpec() {}
30553113
}
30563114

3115+
/**
3116+
* Configures password management.
3117+
*
3118+
* @author Evgeniy Cheban
3119+
* @see #passwordManagement()
3120+
* @since 5.4
3121+
*/
3122+
public class PasswordManagementSpec {
3123+
private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";
3124+
private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";
3125+
3126+
private String changePasswordPage = DEFAULT_CHANGE_PASSWORD_PAGE;
3127+
3128+
/**
3129+
* Sets the change password page.
3130+
* Defaults to {@link PasswordManagementSpec#DEFAULT_CHANGE_PASSWORD_PAGE}.
3131+
*
3132+
* @param changePasswordPage the change password page
3133+
* @return the {@link PasswordManagementSpec} to continue configuring
3134+
*/
3135+
public PasswordManagementSpec changePasswordPage(String changePasswordPage) {
3136+
Assert.hasText(changePasswordPage, "changePasswordPage cannot be empty");
3137+
this.changePasswordPage = changePasswordPage;
3138+
return this;
3139+
}
3140+
3141+
/**
3142+
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}.
3143+
*
3144+
* @return the {@link ServerHttpSecurity} to continue configuring
3145+
*/
3146+
public ServerHttpSecurity and() {
3147+
return ServerHttpSecurity.this;
3148+
}
3149+
3150+
protected void configure(ServerHttpSecurity http) {
3151+
ExchangeMatcherRedirectWebFilter changePasswordWebFilter = new ExchangeMatcherRedirectWebFilter(
3152+
new PathPatternParserServerWebExchangeMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage);
3153+
http.addFilterBefore(changePasswordWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
3154+
}
3155+
3156+
private PasswordManagementSpec() {
3157+
}
3158+
}
3159+
30573160
/**
30583161
* Configures Form Based authentication
30593162
*

0 commit comments

Comments
 (0)