5 min read

In-Memory UserDetailsService in Spring Security

December 22, 2020

So far in the series, we use a default user and a generated password that is found in the startup logs. This is due to the UserDetailsServiceAutoConfiguration trying to fill in for some default values. In this post, we will delve into the concepts of userDetailsService and how it works.

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, The generated password can be replaced with a static password and custom roles can be specified as shown below.

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,

  1. 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.
  2. org.springframework.security.core.userdetails.UserDetailsService - An interface that let you provide UserDetails to the security context. Providing a custom implementation for loadUserByUsername(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");
    }

}

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();
        }

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!";
    }
}

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 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>

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");
    }
}

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. Register and login with in-memory implementation

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 autowiring java.security.Principal. More on this later.