diff --git a/config/src/main/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBean.java
new file mode 100644
index 0000000000..67da735079
--- /dev/null
+++ b/config/src/main/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBean.java
@@ -0,0 +1,132 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.core.userdetails;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ResourceLoaderAware;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.memory.UserAttribute;
+import org.springframework.security.core.userdetails.memory.UserAttributeEditor;
+import org.springframework.util.Assert;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Properties;
+
+/**
+ * Parses a Resource that is a Properties file in the format of:
+ *
+ *
+ * username=password[,enabled|disabled],roles...
+ *
+ *
+ * The enabled and disabled properties are optional with enabled being the default. For example:
+ *
+ *
+ * user=password,ROLE_USER
+ * admin=secret,ROLE_USER,ROLE_ADMIN
+ * disabled_user=does_not_matter,disabled,ROLE_USER
+ *
+ *
+ * @author Rob Winch
+ * @since 5.0
+ */
+public class UserDetailsResourceFactoryBean implements ResourceLoaderAware, FactoryBean> {
+ private ResourceLoader resourceLoader;
+
+ private String propertiesResourceLocation;
+
+ private Resource propertiesResource;
+
+ @Override
+ public void setResourceLoader(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ @Override
+ public Collection getObject() throws Exception {
+ Properties userProperties = new Properties();
+ Resource resource = getProperitesResource();
+ try(InputStream in = resource.getInputStream()){
+ userProperties.load(in);
+ }
+
+ Collection users = new ArrayList<>(userProperties.size());
+ Enumeration> names = userProperties.propertyNames();
+ UserAttributeEditor editor = new UserAttributeEditor();
+
+ while (names.hasMoreElements()) {
+ String name = (String) names.nextElement();
+ String property = userProperties.getProperty(name);
+ editor.setAsText(property);
+ UserAttribute attr = (UserAttribute) editor.getValue();
+ if(attr == null) {
+ throw new IllegalStateException("The entry with username '" + name + "' and value '" + property + "' could not be converted to a UserDetails.");
+ }
+ UserDetails user = User.withUsername(name)
+ .password(attr.getPassword())
+ .disabled(!attr.isEnabled())
+ .authorities(attr.getAuthorities())
+ .build();
+ users.add(user);
+ }
+ return users;
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return Collection.class;
+ }
+
+ /**
+ * Sets a the location of a Resource that is a Properties file in the format defined in {@link UserDetailsResourceFactoryBean}
+ *
+ * @param propertiesResourceLocation the location of the properties file that contains the users (i.e. "classpath:users.properties")
+ */
+ public void setPropertiesResourceLocation(String propertiesResourceLocation) {
+ this.propertiesResourceLocation = propertiesResourceLocation;
+ }
+
+ /**
+ * Sets a a Resource that is a Properties file in the format defined in {@link UserDetailsResourceFactoryBean}
+ *
+ * @param propertiesResource the Resource to use
+ */
+ public void setPropertiesResource(Resource propertiesResource) {
+ this.propertiesResource = propertiesResource;
+ }
+
+ private Resource getProperitesResource() {
+ if(propertiesResource != null) {
+ return propertiesResource;
+ }
+ if(propertiesResourceLocation != null) {
+ Assert.notNull(resourceLoader, "resourceLoader cannot be null if propertiesResource is null");
+ return resourceLoader.getResource(propertiesResourceLocation);
+ }
+ throw new IllegalStateException("Either propertiesResource cannot be null or both resourceLoader and propertiesResourceLocation cannot be null");
+ }
+}
diff --git a/config/src/test/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBeanTest.java b/config/src/test/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBeanTest.java
new file mode 100644
index 0000000000..6df0ed1a4b
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/core/userdetails/UserDetailsResourceFactoryBeanTest.java
@@ -0,0 +1,119 @@
+/*
+ *
+ * * Copyright 2002-2017 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
+ * *
+ * * http://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.core.userdetails;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.util.InMemoryResource;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.Collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+/**
+ * @author Rob Winch
+ * @since 5.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class UserDetailsResourceFactoryBeanTest {
+ String location = "classpath:users.properties";
+
+ @Mock
+ ResourceLoader resourceLoader;
+
+ UserDetailsResourceFactoryBean factory = new UserDetailsResourceFactoryBean();
+
+ @Test
+ public void getObjectWhenResourceLoaderNullThenThrowsIllegalStateException() throws Exception {
+ factory.setPropertiesResourceLocation(location);
+
+ assertThatThrownBy(() -> factory.getObject() )
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasStackTraceContaining("resourceLoader cannot be null if propertiesResource is null");
+ }
+
+ @Test
+ public void getObjectWhenPropertiesResourceLocationNullThenThrowsIllegalStateException() throws Exception {
+ factory.setResourceLoader(resourceLoader);
+
+ assertThatThrownBy(() -> factory.getObject() )
+ .isInstanceOf(IllegalStateException.class)
+ .hasStackTraceContaining("Either propertiesResource cannot be null or both resourceLoader and propertiesResourceLocation cannot be null");
+ }
+
+ @Test
+ public void getObjectWhenPropertiesResourceLocationSingleUserThenThrowsGetsSingleUser() throws Exception {
+ setResource("user=password,ROLE_USER");
+
+ Collection users = factory.getObject();
+
+ UserDetails expectedUser = User.withUsername("user")
+ .password("password")
+ .authorities("ROLE_USER")
+ .build();
+ assertThat(users).containsExactly(expectedUser);
+ }
+
+ @Test
+ public void getObjectWhenPropertiesResourceSingleUserThenThrowsGetsSingleUser() throws Exception {
+ factory.setPropertiesResource(new InMemoryResource("user=password,ROLE_USER"));
+
+ Collection users = factory.getObject();
+
+ UserDetails expectedUser = User.withUsername("user")
+ .password("password")
+ .authorities("ROLE_USER")
+ .build();
+ assertThat(users).containsExactly(expectedUser);
+ }
+
+ @Test
+ public void getObjectWhenInvalidUserThenThrowsMeaningfulException() throws Exception {
+ setResource("user=invalidFormatHere");
+
+
+ assertThatThrownBy(() -> factory.getObject() )
+ .isInstanceOf(IllegalStateException.class)
+ .hasStackTraceContaining("user")
+ .hasStackTraceContaining("invalidFormatHere");
+ }
+
+ private void setResource(String contents) throws IOException {
+ Resource resource = new InMemoryResource(contents);
+ when(resourceLoader.getResource(location)).thenReturn(resource);
+
+ factory.setPropertiesResourceLocation(location);
+ factory.setResourceLoader(resourceLoader);
+ }
+}
diff --git a/config/src/test/resources/users.properties b/config/src/test/resources/users.properties
new file mode 100644
index 0000000000..f1ea5d2cbf
--- /dev/null
+++ b/config/src/test/resources/users.properties
@@ -0,0 +1,19 @@
+#
+# /*
+# * Copyright 2002-2017 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
+# *
+# * http://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.
+# */
+#
+
+user=password,ROLE_USER