jdbcAuthentication() throws Exception {
return super.jdbcAuthentication().passwordEncoder(this.defaultPasswordEncoder);
}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserPasswordDetailsManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserPasswordDetailsManagerConfigurer.java
new file mode 100644
index 00000000000..abfbf3e5460
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/provisioning/JdbcUserPasswordDetailsManagerConfigurer.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2002-2023 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.authentication.configurers.provisioning;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.jdbc.datasource.init.DataSourceInitializer;
+import org.springframework.jdbc.datasource.init.DatabasePopulator;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;
+import org.springframework.security.core.userdetails.UserCache;
+import org.springframework.security.provisioning.JdbcUserPasswordDetailsManager;
+
+/**
+ * Configures an
+ * {@link org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder}
+ * to have JDBC authentication, with user management that will automatically update the
+ * encoding of a password if necessary. It also allows easily adding users to the database
+ * used for authentication and setting up the schema.
+ *
+ *
+ * The only required method is the {@link #dataSource(javax.sql.DataSource)} all other
+ * methods have reasonable defaults.
+ *
+ * @param the type of the {@link ProviderManagerBuilder} that is being configured
+ * @author Rob Winch
+ * @author Geir Hedemark
+ * @since 6.3
+ */
+public final class JdbcUserPasswordDetailsManagerConfigurer>
+ extends UserDetailsManagerConfigurer> {
+
+ private DataSource dataSource;
+
+ private List initScripts = new ArrayList<>();
+
+ public JdbcUserPasswordDetailsManagerConfigurer(JdbcUserPasswordDetailsManager manager) {
+ super(manager);
+ }
+
+ public JdbcUserPasswordDetailsManagerConfigurer() {
+ this(new JdbcUserPasswordDetailsManager());
+ }
+
+ /**
+ * Populates the {@link DataSource} to be used. This is the only required attribute.
+ * @param dataSource the {@link DataSource} to be used. Cannot be null.
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer dataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ getUserDetailsService().setDataSource(dataSource);
+ return this;
+ }
+
+ /**
+ * Sets the query to be used for finding a user by their username. For example:
+ *
+ *
+ * select username,password,enabled from users where username = ?
+ *
+ * @param query The query to use for selecting the username, password, and if the user
+ * is enabled by username. Must contain a single parameter for the username.
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer usersByUsernameQuery(String query) {
+ getUserDetailsService().setUsersByUsernameQuery(query);
+ return this;
+ }
+
+ /**
+ * Sets the query to be used for updating a password for a user. For example:
+ *
+ *
+ * update users set password = ? where username = ?
+ *
+ * @param query The query to use for setting the password for a user. Must contain a
+ * parameter for the password, and one for the username.
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer changePasswordQuery(String query) {
+ getUserDetailsService().setChangePasswordQuery(query);
+ return this;
+ }
+
+ /**
+ * Sets the query to be used for finding a user's authorities by their username. For
+ * example:
+ *
+ *
+ * select username,authority from authorities where username = ?
+ *
+ * @param query The query to use for selecting the username, authority by username.
+ * Must contain a single parameter for the username.
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer authoritiesByUsernameQuery(String query) {
+ getUserDetailsService().setAuthoritiesByUsernameQuery(query);
+ return this;
+ }
+
+ /**
+ * An SQL statement to query user's group authorities given a username. For example:
+ *
+ *
+ * select
+ * g.id, g.group_name, ga.authority
+ * from
+ * groups g, group_members gm, group_authorities ga
+ * where
+ * gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
+ *
+ * @param query The query to use for selecting the authorities by group. Must contain
+ * a single parameter for the username.
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer groupAuthoritiesByUsernameQuery(String query) {
+ JdbcUserPasswordDetailsManager userDetailsService = getUserDetailsService();
+ userDetailsService.setEnableGroups(true);
+ userDetailsService.setGroupAuthoritiesByUsernameQuery(query);
+ return this;
+ }
+
+ /**
+ * A non-empty string prefix that will be added to role strings loaded from persistent
+ * storage (default is "").
+ * @param rolePrefix
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer rolePrefix(String rolePrefix) {
+ getUserDetailsService().setRolePrefix(rolePrefix);
+ return this;
+ }
+
+ /**
+ * Defines the {@link UserCache} to use
+ * @param userCache the {@link UserCache} to use
+ * @return the {@link JdbcUserPasswordDetailsManagerConfigurer} for further
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer userCache(UserCache userCache) {
+ getUserDetailsService().setUserCache(userCache);
+ return this;
+ }
+
+ @Override
+ protected void initUserDetailsService() throws Exception {
+ if (!this.initScripts.isEmpty()) {
+ getDataSourceInit().afterPropertiesSet();
+ }
+ super.initUserDetailsService();
+ }
+
+ @Override
+ public JdbcUserPasswordDetailsManager getUserDetailsService() {
+ return (JdbcUserPasswordDetailsManager) super.getUserDetailsService();
+ }
+
+ /**
+ * Populates the default schema that allows users and authorities to be stored.
+ * @return The {@link JdbcUserPasswordDetailsManagerConfigurer} used for additional
+ * customizations
+ */
+ public JdbcUserPasswordDetailsManagerConfigurer withDefaultSchema() {
+ this.initScripts.add(new ClassPathResource("org/springframework/security/core/userdetails/jdbc/users.ddl"));
+ return this;
+ }
+
+ protected DatabasePopulator getDatabasePopulator() {
+ ResourceDatabasePopulator dbp = new ResourceDatabasePopulator();
+ dbp.setScripts(this.initScripts.toArray(new Resource[0]));
+ return dbp;
+ }
+
+ private DataSourceInitializer getDataSourceInit() {
+ DataSourceInitializer dsi = new DataSourceInitializer();
+ dsi.setDatabasePopulator(getDatabasePopulator());
+ dsi.setDataSource(this.dataSource);
+ return dsi;
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java
index ba962a4fad8..b616a5b230b 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java
@@ -184,7 +184,13 @@ public InMemoryUserDetailsManagerConfigurer inMemo
return super.inMemoryAuthentication().passwordEncoder(this.defaultPasswordEncoder);
}
+ /**
+ * @deprecated Use jdbcAuthenticationWithPasswordManagement instead, as this keeps
+ * the password up to date. Please consult the migration documentation as database
+ * changes might be necessary.
+ */
@Override
+ @Deprecated(since = "For removal in 7.0.")
public JdbcUserDetailsManagerConfigurer jdbcAuthentication() throws Exception {
return super.jdbcAuthentication().passwordEncoder(this.defaultPasswordEncoder);
}
diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParser.java
index 794f30d179a..cd2fb44e10c 100644
--- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationProviderBeanDefinitionParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -50,6 +50,9 @@ public BeanDefinition parse(Element element, ParserContext pc) {
authProvider.getPropertyValues().addPropertyValue("passwordEncoder", passwordEncoder);
}
Element userServiceElt = DomUtils.getChildElementByTagName(element, Elements.USER_SERVICE);
+ if (userServiceElt == null) {
+ userServiceElt = DomUtils.getChildElementByTagName(element, Elements.JDBC_USER_PASSWORD_SERVICE);
+ }
if (userServiceElt == null) {
userServiceElt = DomUtils.getChildElementByTagName(element, Elements.JDBC_USER_SERVICE);
}
diff --git a/config/src/main/java/org/springframework/security/config/authentication/JdbcUserPasswordDetailsManagerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/authentication/JdbcUserPasswordDetailsManagerBeanDefinitionParser.java
new file mode 100644
index 00000000000..f7ebe2c372a
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/authentication/JdbcUserPasswordDetailsManagerBeanDefinitionParser.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2023 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.authentication;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.security.config.Elements;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Luke Taylor
+ * @author Geir Hedemark
+ */
+public final class JdbcUserPasswordDetailsManagerBeanDefinitionParser
+ extends AbstractUserDetailsServiceBeanDefinitionParser {
+
+ static final String ATT_DATA_SOURCE = "data-source-ref";
+ static final String ATT_USERS_BY_USERNAME_QUERY = "users-by-username-query";
+ static final String ATT_AUTHORITIES_BY_USERNAME_QUERY = "authorities-by-username-query";
+ static final String ATT_GROUP_AUTHORITIES_QUERY = "group-authorities-by-username-query";
+ static final String ATT_CHANGE_PASSWORD_QUERY = "change-password-query";
+ static final String ATT_ROLE_PREFIX = "role-prefix";
+
+ @Override
+ protected String getBeanClassName(Element element) {
+ return "org.springframework.security.provisioning.JdbcUserPasswordDetailsManager";
+ }
+
+ @Override
+ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ String dataSource = element.getAttribute(ATT_DATA_SOURCE);
+ if (dataSource != null) {
+ builder.addPropertyReference("dataSource", dataSource);
+ }
+ else {
+ parserContext.getReaderContext()
+ .error(ATT_DATA_SOURCE + " is required for " + Elements.JDBC_USER_PASSWORD_SERVICE,
+ parserContext.extractSource(element));
+ }
+ String usersQuery = element.getAttribute(ATT_USERS_BY_USERNAME_QUERY);
+ String authoritiesQuery = element.getAttribute(ATT_AUTHORITIES_BY_USERNAME_QUERY);
+ String groupAuthoritiesQuery = element.getAttribute(ATT_GROUP_AUTHORITIES_QUERY);
+ String changePasswordQuery = element.getAttribute(ATT_CHANGE_PASSWORD_QUERY);
+ String rolePrefix = element.getAttribute(ATT_ROLE_PREFIX);
+ if (StringUtils.hasText(rolePrefix)) {
+ builder.addPropertyValue("rolePrefix", rolePrefix);
+ }
+ if (StringUtils.hasText(usersQuery)) {
+ builder.addPropertyValue("usersByUsernameQuery", usersQuery);
+ }
+ if (StringUtils.hasText(authoritiesQuery)) {
+ builder.addPropertyValue("authoritiesByUsernameQuery", authoritiesQuery);
+ }
+ if (StringUtils.hasText(groupAuthoritiesQuery)) {
+ builder.addPropertyValue("enableGroups", Boolean.TRUE);
+ builder.addPropertyValue("groupAuthoritiesByUsernameQuery", groupAuthoritiesQuery);
+ }
+ if (StringUtils.hasText(changePasswordQuery)) {
+ builder.addPropertyValue("changePasswordQuery", changePasswordQuery);
+ }
+ }
+
+}
diff --git a/config/src/main/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParser.java
index a0d7de83f65..7e62981dbce 100644
--- a/config/src/main/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParser.java
+++ b/config/src/main/java/org/springframework/security/config/authentication/JdbcUserServiceBeanDefinitionParser.java
@@ -25,7 +25,11 @@
/**
* @author Luke Taylor
+ * @deprecated Use {@link JdbcUserPasswordDetailsManagerBeanDefinitionParser} instead, as
+ * this will update password encodings to more safe variants as users log in
*/
+@Deprecated(
+ since = "For removal in 7.0. Use JdbcUserPasswordDetailsManagerBeanDefinitionParser instead, as this will update password encodings to more safe variants as users log in")
public class JdbcUserServiceBeanDefinitionParser extends AbstractUserDetailsServiceBeanDefinitionParser {
static final String ATT_DATA_SOURCE = "data-source-ref";
diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.3.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-6.3.rnc
index 7f89ced5afd..9a35a6f2be5 100644
--- a/config/src/main/resources/org/springframework/security/config/spring-security-6.3.rnc
+++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.3.rnc
@@ -1123,6 +1123,29 @@ jdbc-user-service.attlist &=
jdbc-user-service.attlist &=
role-prefix?
+jdbc-user-password-service =
+ ## Causes creation of a JDBC-based UserPasswordDetailsService.
+ element jdbc-user-password-service {id? & jdbc-user-password-service.attlist}
+jdbc-user-password-service.attlist &=
+ ## The bean ID of the DataSource which provides the required tables.
+ attribute data-source-ref {xsd:token}
+jdbc-user-password-service.attlist &=
+ cache-ref?
+jdbc-user-password-service.attlist &=
+ ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?"
+ attribute users-by-username-query {xsd:token}?
+jdbc-user-password-service.attlist &=
+ ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?"
+ attribute authorities-by-username-query {xsd:token}?
+jdbc-user-password-service.attlist &=
+ ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"
+ attribute group-authorities-by-username-query {xsd:token}?
+jdbc-user-password-service.attlist &=
+ ## An SQL statement to update a users' password given a username. The default is "update users set password = ? where username = ?"
+ attribute change-password-query {xsd:token}?
+jdbc-user-password-service.attlist &=
+ role-prefix?
+
csrf =
## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests.
element csrf {csrf-options.attlist}
@@ -1321,7 +1344,7 @@ header.attlist &=
## Reference to a custom HeaderWriter implementation.
ref?
-any-user-service = user-service | jdbc-user-service | ldap-user-service
+any-user-service = user-service | jdbc-user-service | jdbc-user-password-service | ldap-user-service
custom-filter =
## Used to indicate that a filter bean declaration should be incorporated into the security filter chain.
diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-6.3.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-6.3.xsd
index f123ad830a7..d63b70006c8 100644
--- a/config/src/main/resources/org/springframework/security/config/spring-security-6.3.xsd
+++ b/config/src/main/resources/org/springframework/security/config/spring-security-6.3.xsd
@@ -3159,6 +3159,72 @@
+
+
+ Causes creation of a JDBC-based UserPasswordDetailsService.
+
+
+
+
+
+ A bean identifier, used for referring to the bean elsewhere in the context.
+
+
+
+
+
+
+
+
+
+ The bean ID of the DataSource which provides the required tables.
+
+
+
+
+
+ Defines a reference to a cache for use with a UserDetailsService.
+
+
+
+
+
+ An SQL statement to query a username, password, and enabled status given a username.
+ Default is "select username,password,enabled from users where username = ?"
+
+
+
+
+
+ An SQL statement to query for a user's granted authorities given a username. The default
+ is "select username, authority from authorities where username = ?"
+
+
+
+
+
+ An SQL statement to query user's group authorities given a username. The default is
+ "select g.id, g.group_name, ga.authority from groups g, group_members gm,
+ group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"
+
+
+
+
+
+ An SQL statement to update a users' password given a username. The default is "update
+ users set password = ? where username = ?"
+
+
+
+
+
+ A non-empty string prefix that will be added to role strings loaded from persistent
+ storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is
+ non-empty.
+
+
+
+
Element for configuration of the CsrfFilter for protection against CSRF. It also updates
diff --git a/config/src/test/java/org/springframework/security/config/authentication/JdbcUserPasswordDetailsManagerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/authentication/JdbcUserPasswordDetailsManagerBeanDefinitionParserTests.java
new file mode 100644
index 00000000000..058674ce466
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/authentication/JdbcUserPasswordDetailsManagerBeanDefinitionParserTests.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2002-2023 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.authentication;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Element;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.CachingUserDetailsService;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.util.InMemoryXmlApplicationContext;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.provisioning.JdbcUserPasswordDetailsManager;
+import org.springframework.security.util.FieldUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * @author Ben Alex
+ * @author Luke Taylor
+ * @author EddĂș MelĂ©ndez
+ * @author Geir Hedemark
+ */
+public class JdbcUserPasswordDetailsManagerBeanDefinitionParserTests {
+
+ private static String USER_CACHE_XML = "";
+
+ // @formatter:off
+ private static String DATA_SOURCE = " "
+ + " "
+ + " "
+ + " "
+ + " "
+ + " ";
+ // @formatter:on
+
+ private InMemoryXmlApplicationContext appContext;
+
+ @AfterEach
+ public void closeAppContext() {
+ if (this.appContext != null) {
+ this.appContext.close();
+ }
+ }
+
+ @Test
+ public void beanNameIsCorrect() {
+ assertThat(JdbcUserPasswordDetailsManager.class.getName())
+ .isEqualTo(new JdbcUserPasswordDetailsManagerBeanDefinitionParser().getBeanClassName(mock(Element.class)));
+ }
+
+ @Test
+ public void validUsernameIsFound() {
+ setContext("" + DATA_SOURCE);
+ JdbcUserPasswordDetailsManager mgr = (JdbcUserPasswordDetailsManager) this.appContext
+ .getBean(BeanIds.USER_DETAILS_SERVICE);
+ assertThat(mgr.loadUserByUsername("rod")).isNotNull();
+ }
+
+ @Test
+ public void beanIdIsParsedCorrectly() {
+ setContext("" + DATA_SOURCE);
+ assertThat(this.appContext.getBean("myUserService") instanceof JdbcUserPasswordDetailsManager).isTrue();
+ }
+
+ @Test
+ public void usernameAndAuthorityQueriesAreParsedCorrectly() throws Exception {
+ String userQuery = "select username, password, true from users where username = ?";
+ String authoritiesQuery = "select username, authority from authorities where username = ? and 1 = 1";
+ // @formatter:off
+ setContext("" + DATA_SOURCE);
+ // @formatter:on
+ JdbcUserPasswordDetailsManager mgr = (JdbcUserPasswordDetailsManager) this.appContext.getBean("myUserService");
+ assertThat(FieldUtils.getFieldValue(mgr, "usersByUsernameQuery")).isEqualTo(userQuery);
+ assertThat(FieldUtils.getFieldValue(mgr, "authoritiesByUsernameQuery")).isEqualTo(authoritiesQuery);
+ assertThat(mgr.loadUserByUsername("rod") != null).isTrue();
+ }
+
+ @Test
+ public void groupQueryIsParsedCorrectly() throws Exception {
+ setContext("" + DATA_SOURCE);
+ JdbcUserPasswordDetailsManager mgr = (JdbcUserPasswordDetailsManager) this.appContext.getBean("myUserService");
+ assertThat(FieldUtils.getFieldValue(mgr, "groupAuthoritiesByUsernameQuery")).isEqualTo("blah blah");
+ assertThat((Boolean) FieldUtils.getFieldValue(mgr, "enableGroups")).isTrue();
+ }
+
+ @Test
+ public void changePasswordQueryIsParsedCorrectly() throws Exception {
+ setContext("" + DATA_SOURCE);
+ JdbcUserPasswordDetailsManager mgr = (JdbcUserPasswordDetailsManager) this.appContext.getBean("myUserService");
+ assertThat(FieldUtils.getFieldValue(mgr, "changePasswordQuery")).isEqualTo("blah blah");
+ }
+
+ @Test
+ public void cacheRefIsparsedCorrectly() {
+ setContext(""
+ + DATA_SOURCE + USER_CACHE_XML);
+ CachingUserDetailsService cachingUserService = (CachingUserDetailsService) this.appContext
+ .getBean("myUserService" + AbstractUserDetailsServiceBeanDefinitionParser.CACHING_SUFFIX);
+ assertThat(this.appContext.getBean("userCache")).isSameAs(cachingUserService.getUserCache());
+ assertThat(cachingUserService.loadUserByUsername("rod")).isNotNull();
+ assertThat(cachingUserService.loadUserByUsername("rod")).isNotNull();
+ }
+
+ @Test
+ public void isSupportedByAuthenticationProviderElement() {
+ // @formatter:off
+ setContext(""
+ + " "
+ + " "
+ + " "
+ + ""
+ + DATA_SOURCE);
+ // @formatter:on
+ AuthenticationManager mgr = (AuthenticationManager) this.appContext.getBean(BeanIds.AUTHENTICATION_MANAGER);
+ mgr.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"));
+ }
+
+ @Test
+ public void cacheIsInjectedIntoAuthenticationProvider() {
+ // @formatter:off
+ setContext(""
+ + " "
+ + " "
+ + " "
+ + ""
+ + DATA_SOURCE
+ + USER_CACHE_XML);
+ // @formatter:on
+ ProviderManager mgr = (ProviderManager) this.appContext.getBean(BeanIds.AUTHENTICATION_MANAGER);
+ DaoAuthenticationProvider provider = (DaoAuthenticationProvider) mgr.getProviders().get(0);
+ assertThat(this.appContext.getBean("userCache")).isSameAs(provider.getUserCache());
+ provider.authenticate(UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"));
+ assertThat(provider.getUserCache().getUserFromCache("rod")).isNotNull()
+ .withFailMessage("Cache should contain user after authentication");
+ }
+
+ @Test
+ public void rolePrefixIsUsedWhenSet() {
+ setContext(""
+ + DATA_SOURCE);
+ JdbcUserPasswordDetailsManager mgr = (JdbcUserPasswordDetailsManager) this.appContext.getBean("myUserService");
+ UserDetails rod = mgr.loadUserByUsername("rod");
+ assertThat(AuthorityUtils.authorityListToSet(rod.getAuthorities())).contains("PREFIX_ROLE_SUPERVISOR");
+ }
+
+ private void setContext(String context) {
+ System.out.println("Context: " + context);
+ this.appContext = new InMemoryXmlApplicationContext(context);
+ }
+
+}
diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
index 2ac71e58266..8d1790bc505 100644
--- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
+++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
@@ -150,9 +150,11 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() throws I
.getParentFile()
.list((dir, name) -> name.endsWith(".xsd"));
// @formatter:on
- assertThat(schemas.length)
- .withFailMessage("the count is equal to 25, if not then schemaDocument needs updating")
- .isEqualTo(25);
+ int expectedCount = 25;
+ assertThat(schemas.length).withFailMessage(String.format(
+ "the count should be equal to %d, not the current %d, if not then schemaDocument needs updating",
+ expectedCount, schemas.length))
+ .isEqualTo(expectedCount);
}
/**
diff --git a/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql b/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql
index 7ca4c6e6975..a3ab2ec0fca 100644
--- a/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql
+++ b/config/src/test/resources/CustomJdbcUserServiceSampleConfig.sql
@@ -1,4 +1,4 @@
-create table users(principal varchar_ignorecase(50) not null primary key,credentials varchar_ignorecase(50) not null);
+create table users(principal varchar_ignorecase(50) not null primary key,credentials varchar_ignorecase(100) not null);
create table roles (principal varchar_ignorecase(50) not null,role varchar_ignorecase(50) not null,constraint fk_roles_users foreign key(principal) references users(principal));
create unique index ix_auth_principal on roles (principal,role);
create table groups (id bigint generated by default as identity(start with 0) primary key,group_name varchar_ignorecase(50) not null);
@@ -10,4 +10,4 @@ insert into roles values('user','USER');
insert into groups values(1,'OPERATIONS');
insert into group_authorities values(1,'DBA');
-insert into group_members values(1,'user',1);
\ No newline at end of file
+insert into group_members values(1,'user',1);
diff --git a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java
index d6a5b7c8878..1fcf2a60e8b 100644
--- a/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java
+++ b/core/src/main/java/org/springframework/security/provisioning/JdbcUserDetailsManager.java
@@ -62,7 +62,11 @@
*
* @author Luke Taylor
* @since 2.0
+ * @deprecated Use {@link JdbcUserPasswordDetailsManager} instead, as this ensure password
+ * encoding is kept updated. Please note that this migration might require changes to your
+ * code and database.
*/
+@Deprecated(since = "For removal in 7.0.")
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {
public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
diff --git a/core/src/main/java/org/springframework/security/provisioning/JdbcUserPasswordDetailsManager.java b/core/src/main/java/org/springframework/security/provisioning/JdbcUserPasswordDetailsManager.java
new file mode 100644
index 00000000000..3f1e4c0432d
--- /dev/null
+++ b/core/src/main/java/org/springframework/security/provisioning/JdbcUserPasswordDetailsManager.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2002-2023 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.provisioning;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContextException;
+import org.springframework.core.log.LogMessage;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.jdbc.core.PreparedStatementSetter;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserCache;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsPasswordService;
+import org.springframework.security.core.userdetails.cache.NullUserCache;
+import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
+import org.springframework.util.Assert;
+
+/**
+ * Jdbc user management manager, based on the same table structure as the base class,
+ * JdbcDaoImpl.
+ *
+ * This manager will automatically keep the password of the user encoded with the current
+ * password encoding, making it easier to manage password security over time.
+ *
+ * Provides CRUD operations for both users and groups. Note that if the
+ * {@link #setEnableAuthorities(boolean) enableAuthorities} property is set to false,
+ * calls to createUser, updateUser and deleteUser will not store the authorities from the
+ * UserDetails or delete authorities for the user. Since this class cannot
+ * differentiate between authorities which were loaded for an individual or for a group of
+ * which the individual is a member, it's important that you take this into account when
+ * using this implementation for managing your users.
+ *
+ * This class is an evolution of the previous JdbcUserDetailsManager, which was part of
+ * spring security since version 2.0
+ *
+ * @author Luke Taylor
+ * @author Geir Hedemark
+ * @since 6.3
+ */
+public final class JdbcUserPasswordDetailsManager extends JdbcDaoImpl
+ implements UserDetailsManager, GroupManager, UserDetailsPasswordService {
+
+ public static final String DEF_CREATE_USER_QUERY = "insert into users (username, password, enabled) values (?,?,?)";
+
+ public static final String DEF_DELETE_USER_QUERY = "delete from users where username = ?";
+
+ public static final String DEF_UPDATE_USER_QUERY = "update users set password = ?, enabled = ? where username = ?";
+
+ public static final String DEF_INSERT_AUTHORITY_QUERY = "insert into authorities (username, authority) values (?,?)";
+
+ public static final String DEF_DELETE_USER_AUTHORITIES_QUERY = "delete from authorities where username = ?";
+
+ public static final String DEF_USER_EXISTS_QUERY = "select username from users where username = ?";
+
+ public static final String DEF_CHANGE_PASSWORD_QUERY = "update users set password = ? where username = ?";
+
+ public static final String DEF_FIND_GROUPS_QUERY = "select group_name from groups";
+
+ public static final String DEF_FIND_USERS_IN_GROUP_QUERY = "select username from group_members gm, groups g "
+ + "where gm.group_id = g.id and g.group_name = ?";
+
+ public static final String DEF_INSERT_GROUP_QUERY = "insert into groups (group_name) values (?)";
+
+ public static final String DEF_FIND_GROUP_ID_QUERY = "select id from groups where group_name = ?";
+
+ public static final String DEF_INSERT_GROUP_AUTHORITY_QUERY = "insert into group_authorities (group_id, authority) values (?,?)";
+
+ public static final String DEF_DELETE_GROUP_QUERY = "delete from groups where id = ?";
+
+ public static final String DEF_DELETE_GROUP_AUTHORITIES_QUERY = "delete from group_authorities where group_id = ?";
+
+ public static final String DEF_DELETE_GROUP_MEMBERS_QUERY = "delete from group_members where group_id = ?";
+
+ public static final String DEF_RENAME_GROUP_QUERY = "update groups set group_name = ? where group_name = ?";
+
+ public static final String DEF_INSERT_GROUP_MEMBER_QUERY = "insert into group_members (group_id, username) values (?,?)";
+
+ public static final String DEF_DELETE_GROUP_MEMBER_QUERY = "delete from group_members where group_id = ? and username = ?";
+
+ public static final String DEF_GROUP_AUTHORITIES_QUERY_QUERY = "select g.id, g.group_name, ga.authority "
+ + "from groups g, group_authorities ga " + "where g.group_name = ? " + "and g.id = ga.group_id ";
+
+ public static final String DEF_DELETE_GROUP_AUTHORITY_QUERY = "delete from group_authorities where group_id = ? and authority = ?";
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
+ .getContextHolderStrategy();
+
+ private String createUserQuery = DEF_CREATE_USER_QUERY;
+
+ private String deleteUserQuery = DEF_DELETE_USER_QUERY;
+
+ private String updateUserQuery = DEF_UPDATE_USER_QUERY;
+
+ private String createAuthorityQuery = DEF_INSERT_AUTHORITY_QUERY;
+
+ private String deleteUserAuthoritiesQuery = DEF_DELETE_USER_AUTHORITIES_QUERY;
+
+ private String userExistsQuery = DEF_USER_EXISTS_QUERY;
+
+ private String changePasswordQuery = DEF_CHANGE_PASSWORD_QUERY;
+
+ private String findAllGroupsQuery = DEF_FIND_GROUPS_QUERY;
+
+ private String findUsersInGroupQuery = DEF_FIND_USERS_IN_GROUP_QUERY;
+
+ private String insertGroupQuery = DEF_INSERT_GROUP_QUERY;
+
+ private String findGroupIdQuery = DEF_FIND_GROUP_ID_QUERY;
+
+ private String insertGroupAuthorityQuery = DEF_INSERT_GROUP_AUTHORITY_QUERY;
+
+ private String deleteGroupQuery = DEF_DELETE_GROUP_QUERY;
+
+ private String deleteGroupAuthoritiesQuery = DEF_DELETE_GROUP_AUTHORITIES_QUERY;
+
+ private String deleteGroupMembersQuery = DEF_DELETE_GROUP_MEMBERS_QUERY;
+
+ private String renameGroupQuery = DEF_RENAME_GROUP_QUERY;
+
+ private String insertGroupMemberQuery = DEF_INSERT_GROUP_MEMBER_QUERY;
+
+ private String deleteGroupMemberQuery = DEF_DELETE_GROUP_MEMBER_QUERY;
+
+ private String groupAuthoritiesQuery = DEF_GROUP_AUTHORITIES_QUERY_QUERY;
+
+ private String deleteGroupAuthorityQuery = DEF_DELETE_GROUP_AUTHORITY_QUERY;
+
+ private AuthenticationManager authenticationManager;
+
+ private UserCache userCache = new NullUserCache();
+
+ public JdbcUserPasswordDetailsManager() {
+ }
+
+ public JdbcUserPasswordDetailsManager(DataSource dataSource) {
+ setDataSource(dataSource);
+ }
+
+ protected void initDao() throws ApplicationContextException {
+ if (this.authenticationManager == null) {
+ this.logger.info(
+ "No authentication manager set. Reauthentication of users when changing passwords will not be performed.");
+ }
+ super.initDao();
+ }
+
+ /**
+ * Executes the SQL usersByUsernameQuery and returns a list of UserDetails
+ * objects. There should normally only be one matching user.
+ */
+ @Override
+ protected List loadUsersByUsername(String username) {
+ return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
+ }
+
+ private UserDetails mapToUser(ResultSet rs, int rowNum) throws SQLException {
+ String userName = rs.getString(1);
+ String password = rs.getString(2);
+ boolean enabled = rs.getBoolean(3);
+ boolean accLocked = false;
+ boolean accExpired = false;
+ boolean credsExpired = false;
+ if (rs.getMetaData().getColumnCount() > 3) {
+ // NOTE: acc_locked, acc_expired and creds_expired are also to be loaded
+ accLocked = rs.getBoolean(4);
+ accExpired = rs.getBoolean(5);
+ credsExpired = rs.getBoolean(6);
+ }
+ return new User(userName, password, enabled, !accExpired, !credsExpired, !accLocked,
+ AuthorityUtils.NO_AUTHORITIES);
+ }
+
+ public void createUser(final UserDetails user) {
+ validateUserDetails(user);
+ getJdbcTemplate().update(this.createUserQuery, (ps) -> {
+ ps.setString(1, user.getUsername());
+ ps.setString(2, user.getPassword());
+ ps.setBoolean(3, user.isEnabled());
+ int paramCount = ps.getParameterMetaData().getParameterCount();
+ if (paramCount > 3) {
+ // NOTE: acc_locked, acc_expired and creds_expired are also to be inserted
+ ps.setBoolean(4, !user.isAccountNonLocked());
+ ps.setBoolean(5, !user.isAccountNonExpired());
+ ps.setBoolean(6, !user.isCredentialsNonExpired());
+ }
+ });
+ if (getEnableAuthorities()) {
+ insertUserAuthorities(user);
+ }
+ }
+
+ public void updateUser(final UserDetails user) {
+ validateUserDetails(user);
+ getJdbcTemplate().update(this.updateUserQuery, (ps) -> {
+ ps.setString(1, user.getPassword());
+ ps.setBoolean(2, user.isEnabled());
+ int paramCount = ps.getParameterMetaData().getParameterCount();
+ if (paramCount == 3) {
+ ps.setString(3, user.getUsername());
+ }
+ else {
+ // NOTE: acc_locked, acc_expired and creds_expired are also updated
+ ps.setBoolean(3, !user.isAccountNonLocked());
+ ps.setBoolean(4, !user.isAccountNonExpired());
+ ps.setBoolean(5, !user.isCredentialsNonExpired());
+ ps.setString(6, user.getUsername());
+ }
+ });
+ if (getEnableAuthorities()) {
+ deleteUserAuthorities(user.getUsername());
+ insertUserAuthorities(user);
+ }
+ this.userCache.removeUserFromCache(user.getUsername());
+ }
+
+ private void insertUserAuthorities(UserDetails user) {
+ for (GrantedAuthority auth : user.getAuthorities()) {
+ getJdbcTemplate().update(this.createAuthorityQuery, user.getUsername(), auth.getAuthority());
+ }
+ }
+
+ public void deleteUser(String username) {
+ if (getEnableAuthorities()) {
+ deleteUserAuthorities(username);
+ }
+ getJdbcTemplate().update(this.deleteUserQuery, username);
+ this.userCache.removeUserFromCache(username);
+ }
+
+ private void deleteUserAuthorities(String username) {
+ getJdbcTemplate().update(this.deleteUserAuthoritiesQuery, username);
+ }
+
+ public void changePassword(String oldPassword, String newPassword) throws AuthenticationException {
+ Authentication currentUser = this.securityContextHolderStrategy.getContext().getAuthentication();
+ if (currentUser == null) {
+ // This would indicate bad coding somewhere
+ throw new AccessDeniedException(
+ "Can't change password as no Authentication object found in context " + "for current user.");
+ }
+ String username = currentUser.getName();
+ // If an authentication manager has been set, re-authenticate the user with the
+ // supplied password.
+ if (this.authenticationManager != null) {
+ this.logger.debug(LogMessage.format("Reauthenticating user '%s' for password change request.", username));
+ this.authenticationManager
+ .authenticate(UsernamePasswordAuthenticationToken.unauthenticated(username, oldPassword));
+ }
+ else {
+ this.logger.debug("No authentication manager set. Password won't be re-checked.");
+ }
+ this.logger.debug("Changing password for user '" + username + "'");
+ getJdbcTemplate().update(this.changePasswordQuery, newPassword, username);
+ Authentication authentication = createNewAuthentication(currentUser, newPassword);
+ SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
+ context.setAuthentication(authentication);
+ this.securityContextHolderStrategy.setContext(context);
+ this.userCache.removeUserFromCache(username);
+ }
+
+ protected Authentication createNewAuthentication(Authentication currentAuth, String newPassword) {
+ UserDetails user = loadUserByUsername(currentAuth.getName());
+ UsernamePasswordAuthenticationToken newAuthentication = UsernamePasswordAuthenticationToken.authenticated(user,
+ null, user.getAuthorities());
+ newAuthentication.setDetails(currentAuth.getDetails());
+ return newAuthentication;
+ }
+
+ @Override
+ public boolean userExists(String username) {
+ List users = getJdbcTemplate().queryForList(this.userExistsQuery, new String[] { username },
+ String.class);
+ if (users.size() > 1) {
+ throw new IncorrectResultSizeDataAccessException("More than one user found with name '" + username + "'",
+ 1);
+ }
+ return users.size() == 1;
+ }
+
+ @Override
+ public List findAllGroups() {
+ return getJdbcTemplate().queryForList(this.findAllGroupsQuery, String.class);
+ }
+
+ @Override
+ public List findUsersInGroup(String groupName) {
+ Assert.hasText(groupName, "groupName should have text");
+ return getJdbcTemplate().queryForList(this.findUsersInGroupQuery, new String[] { groupName }, String.class);
+ }
+
+ @Override
+ public void createGroup(final String groupName, final List authorities) {
+ Assert.hasText(groupName, "groupName should have text");
+ Assert.notNull(authorities, "authorities cannot be null");
+ this.logger.debug("Creating new group '" + groupName + "' with authorities "
+ + AuthorityUtils.authorityListToSet(authorities));
+ getJdbcTemplate().update(this.insertGroupQuery, groupName);
+ int groupId = findGroupId(groupName);
+ for (GrantedAuthority a : authorities) {
+ String authority = a.getAuthority();
+ getJdbcTemplate().update(this.insertGroupAuthorityQuery, (ps) -> {
+ ps.setInt(1, groupId);
+ ps.setString(2, authority);
+ });
+ }
+ }
+
+ @Override
+ public void deleteGroup(String groupName) {
+ this.logger.debug("Deleting group '" + groupName + "'");
+ Assert.hasText(groupName, "groupName should have text");
+ int id = findGroupId(groupName);
+ PreparedStatementSetter groupIdPSS = (ps) -> ps.setInt(1, id);
+ getJdbcTemplate().update(this.deleteGroupMembersQuery, groupIdPSS);
+ getJdbcTemplate().update(this.deleteGroupAuthoritiesQuery, groupIdPSS);
+ getJdbcTemplate().update(this.deleteGroupQuery, groupIdPSS);
+ }
+
+ @Override
+ public void renameGroup(String oldName, String newName) {
+ this.logger.debug("Changing group name from '" + oldName + "' to '" + newName + "'");
+ Assert.hasText(oldName, "oldName should have text");
+ Assert.hasText(newName, "newName should have text");
+ getJdbcTemplate().update(this.renameGroupQuery, newName, oldName);
+ }
+
+ @Override
+ public void addUserToGroup(final String username, final String groupName) {
+ this.logger.debug("Adding user '" + username + "' to group '" + groupName + "'");
+ Assert.hasText(username, "username should have text");
+ Assert.hasText(groupName, "groupName should have text");
+ int id = findGroupId(groupName);
+ getJdbcTemplate().update(this.insertGroupMemberQuery, (ps) -> {
+ ps.setInt(1, id);
+ ps.setString(2, username);
+ });
+ this.userCache.removeUserFromCache(username);
+ }
+
+ @Override
+ public void removeUserFromGroup(final String username, final String groupName) {
+ this.logger.debug("Removing user '" + username + "' to group '" + groupName + "'");
+ Assert.hasText(username, "username should have text");
+ Assert.hasText(groupName, "groupName should have text");
+ int id = findGroupId(groupName);
+ getJdbcTemplate().update(this.deleteGroupMemberQuery, (ps) -> {
+ ps.setInt(1, id);
+ ps.setString(2, username);
+ });
+ this.userCache.removeUserFromCache(username);
+ }
+
+ @Override
+ public List findGroupAuthorities(String groupName) {
+ this.logger.debug("Loading authorities for group '" + groupName + "'");
+ Assert.hasText(groupName, "groupName should have text");
+ return getJdbcTemplate().query(this.groupAuthoritiesQuery, new String[] { groupName },
+ this::mapToGrantedAuthority);
+ }
+
+ private GrantedAuthority mapToGrantedAuthority(ResultSet rs, int rowNum) throws SQLException {
+ String roleName = getRolePrefix() + rs.getString(3);
+ return new SimpleGrantedAuthority(roleName);
+ }
+
+ @Override
+ public void removeGroupAuthority(String groupName, final GrantedAuthority authority) {
+ this.logger.debug("Removing authority '" + authority + "' from group '" + groupName + "'");
+ Assert.hasText(groupName, "groupName should have text");
+ Assert.notNull(authority, "authority cannot be null");
+ int id = findGroupId(groupName);
+ getJdbcTemplate().update(this.deleteGroupAuthorityQuery, (ps) -> {
+ ps.setInt(1, id);
+ ps.setString(2, authority.getAuthority());
+ });
+ }
+
+ @Override
+ public void addGroupAuthority(final String groupName, final GrantedAuthority authority) {
+ this.logger.debug("Adding authority '" + authority + "' to group '" + groupName + "'");
+ Assert.hasText(groupName, "groupName should have text");
+ Assert.notNull(authority, "authority cannot be null");
+ int id = findGroupId(groupName);
+ getJdbcTemplate().update(this.insertGroupAuthorityQuery, (ps) -> {
+ ps.setInt(1, id);
+ ps.setString(2, authority.getAuthority());
+ });
+ }
+
+ private int findGroupId(String group) {
+ return getJdbcTemplate().queryForObject(this.findGroupIdQuery, Integer.class, group);
+ }
+
+ /**
+ * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
+ * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
+ *
+ * @since 5.8
+ */
+ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
+ Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
+ this.securityContextHolderStrategy = securityContextHolderStrategy;
+ }
+
+ public void setAuthenticationManager(AuthenticationManager authenticationManager) {
+ this.authenticationManager = authenticationManager;
+ }
+
+ public void setCreateUserQuery(String createUserQuery) {
+ Assert.hasText(createUserQuery, "createUserQuery should have text");
+ this.createUserQuery = createUserQuery;
+ }
+
+ public void setDeleteUserQuery(String deleteUserQuery) {
+ Assert.hasText(deleteUserQuery, "deleteUserQuery should have text");
+ this.deleteUserQuery = deleteUserQuery;
+ }
+
+ public void setUpdateUserQuery(String updateUserQuery) {
+ Assert.hasText(updateUserQuery, "updateUserQuery should have text");
+ this.updateUserQuery = updateUserQuery;
+ }
+
+ public void setCreateAuthorityQuery(String createAuthorityQuery) {
+ Assert.hasText(createAuthorityQuery, "createAuthorityQuery should have text");
+ this.createAuthorityQuery = createAuthorityQuery;
+ }
+
+ public void setDeleteUserAuthoritiesQuery(String deleteUserAuthoritiesQuery) {
+ Assert.hasText(deleteUserAuthoritiesQuery, "deleteUserAuthoritiesQuery should have text");
+ this.deleteUserAuthoritiesQuery = deleteUserAuthoritiesQuery;
+ }
+
+ public void setUserExistsQuery(String userExistsQuery) {
+ Assert.hasText(userExistsQuery, "userExistsQuery should have text");
+ this.userExistsQuery = userExistsQuery;
+ }
+
+ public void setChangePasswordQuery(String changePasswordQuery) {
+ Assert.hasText(changePasswordQuery, "changePasswordQuery should have text");
+ this.changePasswordQuery = changePasswordQuery;
+ }
+
+ public void setFindAllGroupsQuery(String findAllGroupsQuery) {
+ Assert.hasText(findAllGroupsQuery, "findAllGroupsQuery should have text");
+ this.findAllGroupsQuery = findAllGroupsQuery;
+ }
+
+ public void setFindUsersInGroupQuery(String findUsersInGroupQuery) {
+ Assert.hasText(findUsersInGroupQuery, "findUsersInGroupQuery should have text");
+ this.findUsersInGroupQuery = findUsersInGroupQuery;
+ }
+
+ public void setInsertGroupQuery(String insertGroupQuery) {
+ Assert.hasText(insertGroupQuery, "insertGroupQuery should have text");
+ this.insertGroupQuery = insertGroupQuery;
+ }
+
+ public void setFindGroupIdQuery(String findGroupIdQuery) {
+ Assert.hasText(findGroupIdQuery, "findGroupIdQuery should have text");
+ this.findGroupIdQuery = findGroupIdQuery;
+ }
+
+ public void setInsertGroupAuthorityQuery(String insertGroupAuthorityQuery) {
+ Assert.hasText(insertGroupAuthorityQuery, "insertGroupAuthorityQuery should have text");
+ this.insertGroupAuthorityQuery = insertGroupAuthorityQuery;
+ }
+
+ public void setDeleteGroupQuery(String deleteGroupQuery) {
+ Assert.hasText(deleteGroupQuery, "deleteGroupQuery should have text");
+ this.deleteGroupQuery = deleteGroupQuery;
+ }
+
+ public void setDeleteGroupAuthoritiesQuery(String deleteGroupAuthoritiesQuery) {
+ Assert.hasText(deleteGroupAuthoritiesQuery, "deleteGroupAuthoritiesQuery should have text");
+ this.deleteGroupAuthoritiesQuery = deleteGroupAuthoritiesQuery;
+ }
+
+ public void setDeleteGroupMembersQuery(String deleteGroupMembersQuery) {
+ Assert.hasText(deleteGroupMembersQuery, "deleteGroupMembersQuery should have text");
+ this.deleteGroupMembersQuery = deleteGroupMembersQuery;
+ }
+
+ public void setRenameGroupQuery(String renameGroupQuery) {
+ Assert.hasText(renameGroupQuery, "renameGroupQuery should have text");
+ this.renameGroupQuery = renameGroupQuery;
+ }
+
+ public void setInsertGroupMemberQuery(String insertGroupMemberQuery) {
+ Assert.hasText(insertGroupMemberQuery, "insertGroupMemberQuery should have text");
+ this.insertGroupMemberQuery = insertGroupMemberQuery;
+ }
+
+ public void setDeleteGroupMemberQuery(String deleteGroupMemberQuery) {
+ Assert.hasText(deleteGroupMemberQuery, "deleteGroupMemberQuery should have text");
+ this.deleteGroupMemberQuery = deleteGroupMemberQuery;
+ }
+
+ public void setGroupAuthoritiesQuery(String groupAuthoritiesQuery) {
+ Assert.hasText(groupAuthoritiesQuery, "groupAuthoritiesQuery should have text");
+ this.groupAuthoritiesQuery = groupAuthoritiesQuery;
+ }
+
+ public void setDeleteGroupAuthorityQuery(String deleteGroupAuthorityQuery) {
+ Assert.hasText(deleteGroupAuthorityQuery, "deleteGroupAuthorityQuery should have text");
+ this.deleteGroupAuthorityQuery = deleteGroupAuthorityQuery;
+ }
+
+ /**
+ * Optionally sets the UserCache if one is in use in the application. This allows the
+ * user to be removed from the cache after updates have taken place to avoid stale
+ * data.
+ * @param userCache the cache used by the AuthenticationManager.
+ */
+ public void setUserCache(UserCache userCache) {
+ Assert.notNull(userCache, "userCache cannot be null");
+ this.userCache = userCache;
+ }
+
+ private void validateUserDetails(UserDetails user) {
+ Assert.hasText(user.getUsername(), "Username may not be empty or null");
+ validateAuthorities(user.getAuthorities());
+ }
+
+ private void validateAuthorities(Collection extends GrantedAuthority> authorities) {
+ Assert.notNull(authorities, "Authorities list must not be null");
+ for (GrantedAuthority authority : authorities) {
+ Assert.notNull(authority, "Authorities list contains a null entry");
+ Assert.hasText(authority.getAuthority(), "getAuthority() method must return a non-empty string");
+ }
+ }
+
+ @Override
+ public UserDetails updatePassword(UserDetails user, String newPassword) {
+ this.logger.debug("Updating password for user '" + user.getUsername() + "'");
+ getJdbcTemplate().update(this.changePasswordQuery, newPassword, user.getUsername());
+ this.userCache.removeUserFromCache(user.getUsername());
+ return User.withUserDetails(user).password(newPassword).build();
+ }
+
+}
diff --git a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java
index 67d0c83fa59..5abeb45f6c0 100644
--- a/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java
+++ b/core/src/test/java/org/springframework/security/provisioning/JdbcUserDetailsManagerTests.java
@@ -56,7 +56,12 @@
* Tests for {@link JdbcUserDetailsManager}
*
* @author Luke Taylor
+ * @deprecated JdbcUserDetailsManager has been superseded by
+ * JdbcUserPasswordDetailsManager. If fixing bugs in JdbcUserDetailsManager, please
+ * remember to transplant any new tests to JdbcUserPasswordDetailsManagerTests
+ *
*/
+@Deprecated(since = "For removal in 7.0.")
public class JdbcUserDetailsManagerTests {
private static final String SELECT_JOE_SQL = "select * from users where username = 'joe'";
diff --git a/core/src/test/java/org/springframework/security/provisioning/JdbcUserPasswordDetailsManagerTests.java b/core/src/test/java/org/springframework/security/provisioning/JdbcUserPasswordDetailsManagerTests.java
new file mode 100644
index 00000000000..5fd4a2ba096
--- /dev/null
+++ b/core/src/test/java/org/springframework/security/provisioning/JdbcUserPasswordDetailsManagerTests.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2002-2023 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.provisioning;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.PopulatedDatabase;
+import org.springframework.security.TestDataSource;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserCache;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.verify;
+
+/**
+ * Tests for {@link JdbcUserPasswordDetailsManager}
+ *
+ * @author Geir Hedemark
+ */
+public class JdbcUserPasswordDetailsManagerTests {
+
+ private static final String SELECT_JOE_SQL = "select * from users where username = 'joe'";
+
+ private static final String SELECT_JOE_AUTHORITIES_SQL = "select * from authorities where username = 'joe'";
+
+ private static final UserDetails joe = new User("joe", "password", true, true, true, true,
+ AuthorityUtils.createAuthorityList("A", "C", "B"));
+
+ private static TestDataSource dataSource;
+
+ private JdbcUserPasswordDetailsManager manager;
+
+ private JdbcUserPasswordDetailsManagerTests.MockUserCache cache;
+
+ private JdbcTemplate template;
+
+ @BeforeAll
+ public static void createDataSource() {
+ dataSource = new TestDataSource("jdbcusermgrtest");
+ }
+
+ @AfterAll
+ public static void clearDataSource() throws Exception {
+ dataSource.destroy();
+ dataSource = null;
+ }
+
+ @BeforeEach
+ public void initializeManagerAndCreateTables() {
+ this.manager = new JdbcUserPasswordDetailsManager();
+ this.cache = new JdbcUserPasswordDetailsManagerTests.MockUserCache();
+ this.manager.setUserCache(this.cache);
+ this.manager.setDataSource(dataSource);
+ this.manager.setCreateUserQuery(JdbcUserPasswordDetailsManager.DEF_CREATE_USER_QUERY);
+ this.manager.setUpdateUserQuery(JdbcUserPasswordDetailsManager.DEF_UPDATE_USER_QUERY);
+ this.manager.setUserExistsQuery(JdbcUserPasswordDetailsManager.DEF_USER_EXISTS_QUERY);
+ this.manager.setCreateAuthorityQuery(JdbcUserPasswordDetailsManager.DEF_INSERT_AUTHORITY_QUERY);
+ this.manager.setDeleteUserAuthoritiesQuery(JdbcUserPasswordDetailsManager.DEF_DELETE_USER_AUTHORITIES_QUERY);
+ this.manager.setDeleteUserQuery(JdbcUserPasswordDetailsManager.DEF_DELETE_USER_QUERY);
+ this.manager.setChangePasswordQuery(JdbcUserPasswordDetailsManager.DEF_CHANGE_PASSWORD_QUERY);
+ this.manager.initDao();
+ this.template = this.manager.getJdbcTemplate();
+ this.template.execute("create table users(username varchar(20) not null primary key,"
+ + "password varchar(20) not null, enabled boolean not null)");
+ this.template
+ .execute("create table authorities (username varchar(20) not null, authority varchar(20) not null, "
+ + "constraint fk_authorities_users foreign key(username) references users(username))");
+ PopulatedDatabase.createGroupTables(this.template);
+ PopulatedDatabase.insertGroupData(this.template);
+ }
+
+ @AfterEach
+ public void dropTablesAndClearContext() {
+ this.template.execute("drop table authorities");
+ this.template.execute("drop table users");
+ this.template.execute("drop table group_authorities");
+ this.template.execute("drop table group_members");
+ this.template.execute("drop table groups");
+ SecurityContextHolder.clearContext();
+ }
+
+ private void setUpAccLockingColumns() {
+ this.template.execute("alter table users add column acc_locked boolean default false not null");
+ this.template.execute("alter table users add column acc_expired boolean default false not null");
+ this.template.execute("alter table users add column creds_expired boolean default false not null");
+ this.manager.setUsersByUsernameQuery(
+ "select username,password,enabled, acc_locked, acc_expired, creds_expired from users where username = ?");
+ this.manager.setCreateUserQuery(
+ "insert into users (username, password, enabled, acc_locked, acc_expired, creds_expired) values (?,?,?,?,?,?)");
+ this.manager.setUpdateUserQuery(
+ "update users set password = ?, enabled = ?, acc_locked=?, acc_expired=?, creds_expired=? where username = ?");
+ }
+
+ @Test
+ public void createUserInsertsCorrectData() {
+ this.manager.createUser(joe);
+ UserDetails joe2 = this.manager.loadUserByUsername("joe");
+ assertThat(joe2).isEqualTo(joe);
+ }
+
+ @Test
+ public void createUserInsertsCorrectDataWithLocking() {
+ setUpAccLockingColumns();
+ UserDetails user = new User("joe", "pass", true, false, true, false,
+ AuthorityUtils.createAuthorityList("A", "B"));
+ this.manager.createUser(user);
+ UserDetails user2 = this.manager.loadUserByUsername(user.getUsername());
+ assertThat(user2).usingRecursiveComparison().isEqualTo(user);
+ }
+
+ @Test
+ public void deleteUserRemovesUserDataAndAuthoritiesAndClearsCache() {
+ insertJoe();
+ this.manager.deleteUser("joe");
+ assertThat(this.template.queryForList(SELECT_JOE_SQL)).isEmpty();
+ assertThat(this.template.queryForList(SELECT_JOE_AUTHORITIES_SQL)).isEmpty();
+ assertThat(this.cache.getUserMap().containsKey("joe")).isFalse();
+ }
+
+ @Test
+ public void updateUserChangesDataCorrectlyAndClearsCache() {
+ insertJoe();
+ User newJoe = new User("joe", "newpassword", false, true, true, true,
+ AuthorityUtils.createAuthorityList(new String[] { "D", "F", "E" }));
+ this.manager.updateUser(newJoe);
+ UserDetails joe = this.manager.loadUserByUsername("joe");
+ assertThat(joe).isEqualTo(newJoe);
+ assertThat(this.cache.getUserMap().containsKey("joe")).isFalse();
+ }
+
+ @Test
+ public void updateUserChangesDataCorrectlyAndClearsCacheWithLocking() {
+ setUpAccLockingColumns();
+ insertJoe();
+ User newJoe = new User("joe", "newpassword", false, false, false, true,
+ AuthorityUtils.createAuthorityList("D", "F", "E"));
+ this.manager.updateUser(newJoe);
+ UserDetails joe = this.manager.loadUserByUsername(newJoe.getUsername());
+ assertThat(joe).usingRecursiveComparison().isEqualTo(newJoe);
+ assertThat(this.cache.getUserMap().containsKey(newJoe.getUsername())).isFalse();
+ }
+
+ @Test
+ public void userExistsReturnsFalseForNonExistentUsername() {
+ assertThat(this.manager.userExists("joe")).isFalse();
+ }
+
+ @Test
+ public void userExistsReturnsTrueForExistingUsername() {
+ insertJoe();
+ assertThat(this.manager.userExists("joe")).isTrue();
+ assertThat(this.cache.getUserMap()).containsKey("joe");
+ }
+
+ @Test
+ public void changePasswordFailsForUnauthenticatedUser() {
+ assertThatExceptionOfType(AccessDeniedException.class)
+ .isThrownBy(() -> this.manager.changePassword("password", "newPassword"));
+ }
+
+ @Test
+ public void changePasswordSucceedsWithAuthenticatedUserAndNoAuthenticationManagerSet() {
+ insertJoe();
+ authenticateJoe();
+ this.manager.changePassword("wrongpassword", "newPassword");
+ UserDetails newJoe = this.manager.loadUserByUsername("joe");
+ assertThat(newJoe.getPassword()).isEqualTo("newPassword");
+ assertThat(this.cache.getUserMap().containsKey("joe")).isFalse();
+ }
+
+ @Test
+ public void changePasswordWhenCustomSecurityContextHolderStrategyThenUses() {
+ insertJoe();
+ Authentication authentication = authenticateJoe();
+ SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
+ given(strategy.getContext()).willReturn(new SecurityContextImpl(authentication));
+ given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl());
+ this.manager.setSecurityContextHolderStrategy(strategy);
+ this.manager.changePassword("wrongpassword", "newPassword");
+ verify(strategy).getContext();
+ }
+
+ @Test
+ public void changePasswordSucceedsWithIfReAuthenticationSucceeds() {
+ insertJoe();
+ Authentication currentAuth = authenticateJoe();
+ AuthenticationManager am = mock(AuthenticationManager.class);
+ given(am.authenticate(currentAuth)).willReturn(currentAuth);
+ this.manager.setAuthenticationManager(am);
+ this.manager.changePassword("password", "newPassword");
+ UserDetails newJoe = this.manager.loadUserByUsername("joe");
+ assertThat(newJoe.getPassword()).isEqualTo("newPassword");
+ // The password in the context should also be altered
+ Authentication newAuth = SecurityContextHolder.getContext().getAuthentication();
+ assertThat(newAuth.getName()).isEqualTo("joe");
+ assertThat(newAuth.getDetails()).isEqualTo(currentAuth.getDetails());
+ assertThat(newAuth.getCredentials()).isNull();
+ assertThat(this.cache.getUserMap().containsKey("joe")).isFalse();
+ }
+
+ @Test
+ public void changePasswordFailsIfReAuthenticationFails() {
+ insertJoe();
+ authenticateJoe();
+ AuthenticationManager am = mock(AuthenticationManager.class);
+ given(am.authenticate(any(Authentication.class))).willThrow(new BadCredentialsException(""));
+ this.manager.setAuthenticationManager(am);
+ assertThatExceptionOfType(BadCredentialsException.class)
+ .isThrownBy(() -> this.manager.changePassword("password", "newPassword"));
+ // Check password hasn't changed.
+ UserDetails newJoe = this.manager.loadUserByUsername("joe");
+ assertThat(newJoe.getPassword()).isEqualTo("password");
+ assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("password");
+ assertThat(this.cache.getUserMap()).containsKey("joe");
+ }
+
+ @Test
+ public void findAllGroupsReturnsExpectedGroupNames() {
+ List groups = this.manager.findAllGroups();
+ assertThat(groups).hasSize(4);
+ Collections.sort(groups);
+ assertThat(groups.get(0)).isEqualTo("GROUP_0");
+ assertThat(groups.get(1)).isEqualTo("GROUP_1");
+ assertThat(groups.get(2)).isEqualTo("GROUP_2");
+ assertThat(groups.get(3)).isEqualTo("GROUP_3");
+ }
+
+ @Test
+ public void findGroupMembersReturnsCorrectData() {
+ List groupMembers = this.manager.findUsersInGroup("GROUP_0");
+ assertThat(groupMembers).hasSize(1);
+ assertThat(groupMembers.get(0)).isEqualTo("jerry");
+ groupMembers = this.manager.findUsersInGroup("GROUP_1");
+ assertThat(groupMembers).hasSize(2);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void createGroupInsertsCorrectData() {
+ this.manager.createGroup("TEST_GROUP", AuthorityUtils.createAuthorityList("ROLE_X", "ROLE_Y"));
+ List roles = this.template.queryForList("select ga.authority from groups g, group_authorities ga "
+ + "where ga.group_id = g.id " + "and g.group_name = 'TEST_GROUP'");
+ assertThat(roles).hasSize(2);
+ }
+
+ @Test
+ public void deleteGroupRemovesData() {
+ this.manager.deleteGroup("GROUP_0");
+ this.manager.deleteGroup("GROUP_1");
+ this.manager.deleteGroup("GROUP_2");
+ this.manager.deleteGroup("GROUP_3");
+ assertThat(this.template.queryForList("select * from group_authorities")).isEmpty();
+ assertThat(this.template.queryForList("select * from group_members")).isEmpty();
+ assertThat(this.template.queryForList("select id from groups")).isEmpty();
+ }
+
+ @Test
+ public void renameGroupIsSuccessful() {
+ this.manager.renameGroup("GROUP_0", "GROUP_X");
+ assertThat(this.template.queryForObject("select id from groups where group_name = 'GROUP_X'", Integer.class))
+ .isZero();
+ }
+
+ @Test
+ public void addingGroupUserSetsCorrectData() {
+ this.manager.addUserToGroup("tom", "GROUP_0");
+ assertThat(this.template.queryForList("select username from group_members where group_id = 0")).hasSize(2);
+ }
+
+ @Test
+ public void removeUserFromGroupDeletesGroupMemberRow() {
+ this.manager.removeUserFromGroup("jerry", "GROUP_1");
+ assertThat(this.template.queryForList("select group_id from group_members where username = 'jerry'"))
+ .hasSize(1);
+ }
+
+ @Test
+ public void findGroupAuthoritiesReturnsCorrectAuthorities() {
+ assertThat(AuthorityUtils.createAuthorityList("ROLE_A"))
+ .isEqualTo(this.manager.findGroupAuthorities("GROUP_0"));
+ }
+
+ @Test
+ public void addGroupAuthorityInsertsCorrectGroupAuthorityRow() {
+ GrantedAuthority auth = new SimpleGrantedAuthority("ROLE_X");
+ this.manager.addGroupAuthority("GROUP_0", auth);
+ this.template.queryForObject(
+ "select authority from group_authorities where authority = 'ROLE_X' and group_id = 0", String.class);
+ }
+
+ @Test
+ public void deleteGroupAuthorityRemovesCorrectRows() {
+ GrantedAuthority auth = new SimpleGrantedAuthority("ROLE_A");
+ this.manager.removeGroupAuthority("GROUP_0", auth);
+ assertThat(this.template.queryForList("select authority from group_authorities where group_id = 0")).isEmpty();
+ this.manager.removeGroupAuthority("GROUP_2", auth);
+ assertThat(this.template.queryForList("select authority from group_authorities where group_id = 2")).hasSize(2);
+ }
+
+ // SEC-1156
+ @Test
+ public void createUserDoesNotSaveAuthoritiesIfEnableAuthoritiesIsFalse() {
+ this.manager.setEnableAuthorities(false);
+ this.manager.createUser(joe);
+ assertThat(this.template.queryForList(SELECT_JOE_AUTHORITIES_SQL)).isEmpty();
+ }
+
+ // SEC-1156
+ @Test
+ public void updateUserDoesNotSaveAuthoritiesIfEnableAuthoritiesIsFalse() {
+ this.manager.setEnableAuthorities(false);
+ insertJoe();
+ this.template.execute("delete from authorities where username='joe'");
+ this.manager.updateUser(joe);
+ assertThat(this.template.queryForList(SELECT_JOE_AUTHORITIES_SQL)).isEmpty();
+ }
+
+ // SEC-2166
+ @Test
+ public void createNewAuthenticationUsesNullPasswordToKeepPassordsSave() {
+ insertJoe();
+ UsernamePasswordAuthenticationToken currentAuth = UsernamePasswordAuthenticationToken.authenticated("joe", null,
+ AuthorityUtils.createAuthorityList("ROLE_USER"));
+ Authentication updatedAuth = this.manager.createNewAuthentication(currentAuth, "new");
+ assertThat(updatedAuth.getCredentials()).isNull();
+ }
+
+ @Test
+ public void updatePasswordSucceeds() {
+ insertJoe();
+ UserDetails joe = this.manager.loadUserByUsername("joe");
+ UserDetails returnedJoe = ((JdbcUserPasswordDetailsManager) this.manager).updatePassword(joe, "newPassword");
+ assertThat(returnedJoe.getPassword()).isEqualTo("newPassword");
+ UserDetails newJoe = this.manager.loadUserByUsername("joe");
+ assertThat(newJoe.getPassword()).isEqualTo("newPassword");
+ assertThat(this.cache.getUserMap().containsKey("joe")).isFalse();
+ }
+
+ private Authentication authenticateJoe() {
+ UsernamePasswordAuthenticationToken auth = UsernamePasswordAuthenticationToken.authenticated("joe", "password",
+ joe.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ return auth;
+ }
+
+ private void insertJoe() {
+ this.template.execute("insert into users (username, password, enabled) values ('joe','password','true')");
+ this.template.execute("insert into authorities (username, authority) values ('joe','A')");
+ this.template.execute("insert into authorities (username, authority) values ('joe','B')");
+ this.template.execute("insert into authorities (username, authority) values ('joe','C')");
+ this.cache.putUserInCache(joe);
+ }
+
+ private class MockUserCache implements UserCache {
+
+ private Map cache = new HashMap<>();
+
+ @Override
+ public UserDetails getUserFromCache(String username) {
+ return this.cache.get(username);
+ }
+
+ @Override
+ public void putUserInCache(UserDetails user) {
+ this.cache.put(user.getUsername(), user);
+ }
+
+ @Override
+ public void removeUserFromCache(String username) {
+ this.cache.remove(username);
+ }
+
+ Map getUserMap() {
+ return this.cache;
+ }
+
+ }
+
+}
diff --git a/docs/modules/ROOT/pages/migration-7/jdbcusers.adoc b/docs/modules/ROOT/pages/migration-7/jdbcusers.adoc
new file mode 100644
index 00000000000..5969c2d63b3
--- /dev/null
+++ b/docs/modules/ROOT/pages/migration-7/jdbcusers.adoc
@@ -0,0 +1,66 @@
+= JDBC-based user storage
+
+The existing JdbcUserDetailsManager has been extended to update the encoding of passwords during login
+if the existing password encoding can be improved. The new behaviour is located in
+JdbcUserPasswordDetailsManager because existing users might get into problems when the password is
+re-encoded if the password field is too short.
+
+There are no configuration changes necessary to the bean itself.
+
+== Excending the size of the password field
+
+If the example supplied with spring security is used it is suggested that the size of the password
+field is doubled:
+
+....
+alter table users alter column credentials varchar_ignorecase(100);
+....
+
+If a database such as postgres is used, the following will accept any future encodings which require
+even more space:
+
+....
+alter table users alter column credentials text;
+....
+
+== Configuring the new JdbcUserPasswordDetailsManager
+
+=== Existing DSL configuration
+
+Spring has historically had a DSL to configure spring security. To configure a `+JdbcUserDetailsManager+`
+one invoked the function `+AuthenticationManagerBuilder.jdbcAuthentication+`. If using this kind of
+configuration it is suggested to move to one of the following two configuration options instead. There
+is no DSL-based feature that allows configuration of the new `+JdbcUserPasswordDetailsManager+`.
+
+=== Bean-based configuration
+
+The new user details implementation is mostly a drop-in replacement for the old implementation:
+
+....
+@Bean
+UserDetailsManager users(DataSource dataSource) {
+ return new JdbcUserPasswordDetailsManager(dataSource);
+}
+....
+
+All properties that were previously named `+*Sql+` has been renamed to be `+*Query+` to align with the
+XSD namespace.
+
+There is an additional configuration parameter `+changePasswordQuery+` available for changing the password of a user. This query
+is necessary for the new behaviour of the user details implementation.
+
+Please refer to
+https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/user-details-service.html[the spring security reference documentation]
+for more details.
+
+=== XML-based configuration
+
+Where the XML configuration format previously referred to a `+jdbc-user-service+` tag, there is now a new tag that will trigger
+creation of the new bean:
+
+....
+
+....
+
+The configuration settings are the same as for the old bean, but now also allows setting an sql query
+for changing the password of the user through the `+change-password-query+` attribute.
diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc
index 3719fe7394c..502276e25ba 100644
--- a/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc
+++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc
@@ -94,13 +94,13 @@ A reference to a bean that implements UserDetailsService that may be created usi
=== Child Elements of
+* <>
* <>
* xref:servlet/appendix/namespace/ldap.adoc#nsa-ldap-user-service[ldap-user-service]
* <>
* <>
-
[[nsa-jdbc-user-service]]
==
Causes creation of a JDBC-based UserDetailsService.
@@ -178,6 +178,93 @@ select username, password, enabled from users where username = ?
----
+[[nsa-jdbc-user-password-service]]
+==
+Causes creation of a JDBC-based UserDetailsService. This service will automatically update the encoding of
+a password that is stored with an outdated/insecure encoding.
+
+
+[[nsa-jdbc-user-password-service-attributes]]
+=== Attributes
+
+
+[[nsa-jdbc-user-password-service-authorities-by-username-query]]
+* **authorities-by-username-query**
+An SQL statement to query for a user's granted authorities given a username.
+
+The default is
+
+[source]
+----
+select username, authority from authorities where username = ?
+----
+
+
+
+
+[[nsa-jdbc-user-password-service-cache-ref]]
+* **cache-ref**
+Defines a reference to a cache for use with a UserDetailsService.
+
+
+[[nsa-jdbc-user-password-service-data-source-ref]]
+* **data-source-ref**
+The bean ID of the DataSource which provides the required tables.
+
+
+[[nsa-jdbc-user-password-service-group-authorities-by-username-query]]
+* **group-authorities-by-username-query**
+An SQL statement to query user's group authorities given a username.
+The default is
+
++
+
+[source]
+----
+select
+g.id, g.group_name, ga.authority
+from
+groups g, group_members gm, group_authorities ga
+where
+gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
+----
+
+[[nsa-jdbc-user-password-service-change-password-query]]
+* **change-password-query**
+An SQL statement to update the users password.
+The default is
+
++
+
+[source]
+----
+update users set password = ? where username = ?
+----
+
+[[nsa-jdbc-user-password-service-id]]
+* **id**
+A bean identifier, used for referring to the bean elsewhere in the context.
+
+
+[[nsa-jdbc-user-password-service-role-prefix]]
+* **role-prefix**
+A non-empty string prefix that will be added to role strings loaded from persistent storage (default is "ROLE_").
+Use the value "none" for no prefix in cases where the default is non-empty.
+
+
+[[nsa-jdbc-user-password-service-users-by-username-query]]
+* **users-by-username-query**
+An SQL statement to query a username, password, and enabled status given a username.
+The default is
+
++
+
+[source]
+----
+select username, password, enabled from users where username = ?
+----
+
+
[[nsa-password-encoder]]
diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc
index 2a2d4dfa1ec..e0d08775b54 100644
--- a/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc
+++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/jdbc.adoc
@@ -2,7 +2,7 @@
= JDBC Authentication
Spring Security's `JdbcDaoImpl` implements xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] to provide support for username-and-password-based authentication that is retrieved by using JDBC.
-`JdbcUserDetailsManager` extends `JdbcDaoImpl` to provide management of `UserDetails` through the `UserDetailsManager` interface.
+`JdbcUserPasswordDetailsManager` extends `JdbcDaoImpl` to provide management of `UserDetails` through the `UserDetailsManager` interface.
`UserDetails`-based authentication is used by Spring Security when it is configured to xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd-input[accept a username/password] for authentication.
In the following sections, we discuss:
@@ -95,7 +95,7 @@ create table group_members (
[[servlet-authentication-jdbc-datasource]]
== Setting up a DataSource
-Before we configure `JdbcUserDetailsManager`, we must create a `DataSource`.
+Before we configure `JdbcUserPasswordDetailsManager`, we must create a `DataSource`.
In our example, we set up an https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#jdbc-embedded-database-support[embedded DataSource] that is initialized with the <>.
.Embedded Data Source
@@ -140,12 +140,12 @@ fun dataSource(): DataSource {
In a production environment, you want to ensure that you set up a connection to an external database.
[[servlet-authentication-jdbc-bean]]
-== JdbcUserDetailsManager Bean
+== JdbcUserPasswordDetailsManager Bean
In this sample, we use xref:features/authentication/password-storage.adoc#authentication-password-storage-boot-cli[Spring Boot CLI] to encode a password value of `password` and get the encoded password of `+{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW+`.
See the xref:features/authentication/password-storage.adoc#authentication-password-storage[PasswordEncoder] section for more details about how to store passwords.
-.JdbcUserDetailsManager
+.JdbcUserPasswordDetailsManager
[tabs]
======
@@ -165,7 +165,7 @@ UserDetailsManager users(DataSource dataSource) {
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
- JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
+ JdbcUserPasswordDetailsManager users = new JdbcUserPasswordDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
@@ -176,14 +176,14 @@ XML::
+
[source,xml,role="secondary",attrs="-attributes"]
----
-
+
-
+
----
Kotlin::
@@ -202,7 +202,7 @@ fun users(dataSource: DataSource): UserDetailsManager {
.password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
- val users = JdbcUserDetailsManager(dataSource)
+ val users = JdbcUserPasswordDetailsManager(dataSource)
users.createUser(user)
users.createUser(admin)
return users
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java
index cf3e5fb206b..fe6996aedf1 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java
@@ -171,7 +171,7 @@ private NimbusJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
}
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm)
- .restOperations(restOperationsFactory.apply(clientRegistration))
+ .restOperations(this.restOperationsFactory.apply(clientRegistration))
.build();
}
if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java
index 0e2b2b2ddd9..1cc8e5a6e44 100644
--- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java
+++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java
@@ -168,7 +168,7 @@ private NimbusReactiveJwtDecoder buildDecoder(ClientRegistration clientRegistrat
}
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
.jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm)
- .webClient(webClientFactory.apply(clientRegistration))
+ .webClient(this.webClientFactory.apply(clientRegistration))
.build();
}
if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java
index b417d17b676..52db517f5c9 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java
@@ -100,7 +100,7 @@ public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentExceptio
@Test
public void setRestOperationsFactoryWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
- .isThrownBy(() -> this.idTokenDecoderFactory.setRestOperationsFactory(null));
+ .isThrownBy(() -> this.idTokenDecoderFactory.setRestOperationsFactory(null));
}
@Test
@@ -187,13 +187,12 @@ public void createDecoderWhenCustomClaimTypeConverterFactorySetThenApplied() {
@Test
public void createDecoderWhenCustomRestOperationsFactorySetThenApplied() {
- Function customRestOperationsFactory = mock(
- Function.class);
+ Function customRestOperationsFactory = mock(Function.class);
this.idTokenDecoderFactory.setRestOperationsFactory(customRestOperationsFactory);
ClientRegistration clientRegistration = this.registration.build();
- given(customRestOperationsFactory.apply(same(clientRegistration)))
- .willReturn(new RestTemplate());
+ given(customRestOperationsFactory.apply(same(clientRegistration))).willReturn(new RestTemplate());
this.idTokenDecoderFactory.createDecoder(clientRegistration);
verify(customRestOperationsFactory).apply(same(clientRegistration));
}
+
}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java
index 87f7dd67f23..8f304e098d2 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java
@@ -97,8 +97,7 @@ public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentExceptio
@Test
public void setWebClientFactoryWhenNullThenThrowIllegalArgumentException() {
- assertThatIllegalArgumentException()
- .isThrownBy(() -> this.idTokenDecoderFactory.setWebClientFactory(null));
+ assertThatIllegalArgumentException().isThrownBy(() -> this.idTokenDecoderFactory.setWebClientFactory(null));
}
@Test
@@ -185,13 +184,12 @@ public void createDecoderWhenCustomClaimTypeConverterFactorySetThenApplied() {
@Test
public void createDecoderWhenCustomWebClientFactorySetThenApplied() {
- Function customWebClientFactory = mock(
- Function.class);
+ Function customWebClientFactory = mock(Function.class);
this.idTokenDecoderFactory.setWebClientFactory(customWebClientFactory);
ClientRegistration clientRegistration = this.registration.build();
- given(customWebClientFactory.apply(same(clientRegistration)))
- .willReturn(WebClient.create());
+ given(customWebClientFactory.apply(same(clientRegistration))).willReturn(WebClient.create());
this.idTokenDecoderFactory.createDecoder(clientRegistration);
verify(customWebClientFactory).apply(same(clientRegistration));
}
+
}