In-Memory UserDetailsService in Spring Security
In this post, we will take a look at how the default in-memory UserDetailsService works in Spring Boot application.
Default behaviour
The default autoconfiguration provides an InMemoryUserDetailsManager
that generates a single user for the application to support. We can override these user properties to an extent with changes to application.properties
file. For instance, you can change the default username password roles etc.
spring.security.user.name=admin
spring.security.user.password=Admin@123
spring.security.user.roles=USER,ADMIN
But the configuration can only get us to that much. If we wanted multiple users within our servers, we need to provide our implementation of an UserDetailsService
.
What is UserDetailService
When a form of credential is presented to the server, it needs to look up somewhere to find the information about the user. For example, The server can lookup in a database table for a record, an entry in LDAP or AD, or a simple text file containing a list of user information. To provide a common way to do this, Spring Security provides two main interfaces. They are,
org.springframework.security.core.userdetails.UserDetails
– A representation of user information including but not limited to username, password, account status, and the roles associated to the user etc. If you want to provide your implementation for user information to the security context, you need to implement this interface.org.springframework.security.core.userdetails.UserDetailsService
– An interface that let you provideUserDetails
to the security context. Providing a custom implementation forloadUserByUsername(String userName)
.
There are various ways to implement both of these classes. Let’s go through them one by one.
In Memory UserDetailService
The default in-memory implementation can be overridden with WebSecurityConfigurerAdapter
. For example, here is a simple implementation.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("admin").password("admin1pass").roles("USER", "ADMIN").and()
.withUser("user1").password("user1pass").roles("USER").and()
.withUser("user2").password("user2pass").roles("USER").and()
.withUser("user3").password("user3pass").roles("USER");
}
}
Code language: CSS (css)
The above code uses an InMemoryUserDetailsManager
with pre-populated list users and their roles. With this change, you can log in as any of the users mentioned in the configuration.
Even though this is okay for some demo, in the real world, users are not pre-configured in the application code. Instead, users would get onboarded through registration and loaded from DB or similar.
InMemoryUserDetailsManager with dynamic users.
the good thing about the auto-configuration is that any bean of type UserDetailsService will automatically be wired to the Authentication managers. In this case, I defined a simple InMemoryUserDetailsManager
so that I can add or remove users from it on the fly.
Anywhere at your @Configuration class, add the following.
@Bean
public InMemoryUserDetailsManager getInMemoryUserDetailsManager(){
return new InMemoryUserDetailsManager();
}
Code language: PHP (php)
If you look at the implementation of InMemoryUserDetailsManager
it is a type of UserDetailsService and has methods to create and manage user entries. So let’s add logic to create users.
@RestController
public class UserController {
private final InMemoryUserDetailsManager inMemoryUserDetailsManager;
public UserController(InMemoryUserDetailsManager inMemoryUserDetailsManager) {
this.inMemoryUserDetailsManager = inMemoryUserDetailsManager;
}
@PostMapping("/register")
public String register(@RequestParam("username") String username, @RequestParam("password") String password) {
inMemoryUserDetailsManager.createUser(User.withUsername(username).password("{noop}" + password).roles("USER").build());
return username + " Created!";
}
}
Code language: PHP (php)
This /register
endpoint will help us add users to the system dynamically. Even though the builder methods have a lot to offer, I’m restricting the use to just username, password and a default role.
Note how the password is prepended withÂ
{noop}
. This is due to how the passwords are handled since spring security 5. For now, just know that whatever follows {noop} is a plain text password. You may want to look at understanding password encoders for more on this topic.
Because the endpoint won’t call itself, I created a simple registration form at src/main/resources/static/register.html
. Here is the content of that file.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Register a new user</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css"/>
</head>
<body class="container mt-5">
<h1 class="text-center">Register</h1>
<form method="post" action="/register" autocomplete="off">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" autocomplete="new-password"
placeholder="Enter username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password"
autocomplete="new-password">
</div>
<input type="submit" class="btn btn-primary btn-block btn-lg"/>
</form>
</body>
</html>
Code language: HTML, XML (xml)
If you start the application at this point, you will not get anything done. This is because spring security protects all URLs by default. So hitting /register.html
would result in a login page and the /register
endpoint will not be accessible as well due to the same reason. To avoid that, you need to disable web security for these resources. Here is how you can do that.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.POST, "/register")
.antMatchers(HttpMethod.GET, "/register.html");
}
}
Code language: PHP (php)
By doing the above, You are instructing spring security to not guard these URLs. With all the above in place, let’s register a user and try login.
Looks like it all worked out. Right? In the next post, we will see how we can load and save users from the database.
Note that the
/hello
endpoint returns the username of the logged-in user. This is because we can get hold of the current security context through autowiringjava.security.Principal
. More on this later.
You can find this demo in this Github repository.