Learn about the role of UserDetailsService
, default configuration provided by Spring security and further customizing it according to requirements.
1. UserDetailsService in Spring Security Architecture
The below given picture shows the main actors in the Spring Security architecture and the relationships among them. This architecture is the core concept of implementing authentication with Spring Security.
As shown in the image, the AuthenticationFilter delegates the authentication request to the AuthenticationManager which uses AuthenticationProvider to process authentication.
The AuthenticationProvider uses UserDetailsService that implements the user management responsibility. Its primary responsibility is to find a user by its username from the cache or underlying storage.
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
2. Default Implementation of UserDetailsService
A default implementation of the AuthenticationProvider uses the default implementations provided for the UserDetailsService and the PasswordEncoder.
The default implementation of UserDetailsService only registers the default credentials in the memory of the application. These default credentials are "user"
with a default password that’s a randomly generated universally unique identifier (UUID) written to the application console when the spring context loads.
Using generated security password: 78nh23h-sd56-4b98-86ef-dfas8f8asf8
Note that UserDetailsService is always associated with a PasswordEncoder
that encodes a supplied password and verifies if the password matches an existing encoding. When we replace the default implementation of the UserDetailsService, we must also specify a PasswordEncoder.
3. In-memory UserDetailsService
The first very basic example of overriding the UserDetailsService is InMemoryUserDetailsManager. This class stores credentials in the memory, which can then be used by Spring Security to authenticate an incoming request.
A UserDetailsManager extends the UserDetailsService contract. Beyond the inherited behavior, it also provides the methods for creating a user and modifying or deleting a user’s password. The UserDetailsService is only responsible for retrieving the user by username.
@Configuration
public class AppConfig {
@Bean
public UserDetailsService userDetailsService() {
var userDetailsService =
new InMemoryUserDetailsManager();
var user = User.withUsername("user")
.password("password")
.authorities("USER_ROLE")
.build();
userDetailsService.createUser(user);
return userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
The above configuration registers the beans of type UserDetailsService and PasswordEncoder into the spring context and the authentication provider uses them automatically. Spring allows us to set the user service and password encoder directly to the authentication manager if we prefer to do so.
The previous configuration can be re-written as follows:
@Configuration
class AppConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
var userDetailsService = new InMemoryUserDetailsManager();
var user = User.withUsername("user")
.password("password")
.authorities("USER_ROLE")
.build();
userDetailsService.createUser(user);
auth.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
We can verify the above configuration with a simple test.
@Test
public void expectOKResponse_WhenAuthenticaionManagerIsTestedWithCorrectDetails() {
AuthenticationManager authenticationManager = this.spring.getContext()
.getBean(AuthenticationManager.class);
Authentication authentication = authenticationManager
.authenticate(UsernamePasswordAuthenticationToken
.unauthenticated("user", "password"));
assertThat(authentication.isAuthenticated()).isTrue();
}
Note that InMemoryUserDetailsManager
isn’t meant for production-ready applications. Use this class only for examples or proof of concepts.
4. Database-backed UserDetailsService
To store and retrieve the username and passwords from a SQL database, we use JdbcUserDetailsManager
class. It connects to the database directly through JDBC.
By default, it creates two tables in the database:
- USERS
- AUTHORITIES
The default schema file is in the file users.ddl
. The file location is given in the constant JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION
.
To customize the default schema and create initial users, we can write our own schema.sql
and data.sql
files and place them in the application’s /src/main/resources folder. Spring Boot automatically runs these files when we start the application.
Note that the JdbcUserDetailsManager needs a DataSource to connect to the database so we need to define it as well.
@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
.build();
}
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
UserDetails user = User
.withUsername("user")
.password("password")
.roles("USER_ROLE")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
return users;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
We can also write custom SQL queries to fetch the user and authorities’ details if we are using a custom DDL schema that uses different table or column names.
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
String usersByUsernameQuery = "select username, password, enabled from tbl_users where username = ?";
String authsByUserQuery = "select username, authority from tbl_authorities where username = ?";
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return users;
}
5. Conclusion
In this tutorial, we learned about the basic architecture of spring security-based authentication. We learned about the contract that spring security expects from UserDetailsService
and PasswordEncoder
interfaces.
We learned how UserDetailsManager extends UserDetailsService and provides the capability to create users and modify the passwords.
We also learned the initial defaults and basic customizations of InMemoryUserDetailsManager
and JdbcUserDetailsManager
classes.
Happy Learning !!
Leave a Reply