In-Memory Authentication
In-memory authentication stores user accounts in a Map held in memory rather than in a database. It is the fastest way to give your application real, named users with roles, and it is ideal for demos, prototypes, documentation samples, and integration tests where standing up a database would be overkill. For anything production-bound, switch to database authentication.
The UserDetailsService bean
Spring Security looks for a UserDetailsService bean to load users by username. For in-memory users you provide an InMemoryUserDetailsManager populated with UserDetails objects.
import org.springframework.context.annotation.*;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User.builder()
.username("alice")
.password(encoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("bob")
.password(encoder.encode("admin123"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Defining a UserDetailsService bean replaces the default generated user account. Spring Boot’s auto-configured DaoAuthenticationProvider automatically uses your bean together with the PasswordEncoder.
User.builder()
User.builder() is the canonical way to construct a UserDetails. The important methods:
| Method | Purpose |
|---|---|
.username(...) | The login name |
.password(...) | The encoded password (never plaintext) |
.roles("USER") | Adds authorities with the ROLE_ prefix → ROLE_USER |
.authorities("orders:read") | Adds authorities verbatim, no prefix |
.disabled(true) | Marks the account disabled |
.accountLocked(true) | Marks the account locked |
Warning:
.roles("USER")and.authorities("ROLE_USER")produce the same result, but do not mix them..roles("ROLE_USER")throws —roles()adds the prefix itself, so pass"USER".
Encoding passwords
Even for in-memory users you must store an encoded password. Use the injected PasswordEncoder as shown above. The lazy shortcut User.withDefaultPasswordEncoder() exists but is deprecated and unsafe — avoid it.
// Good — encode with the configured encoder
.password(encoder.encode("password"))
// Avoid — deprecated, embeds plaintext intent in code
User.withDefaultPasswordEncoder().username("x").password("password").roles("USER").build();
See Password Encoding for why this matters.
Putting it together
Pair the user store with a SecurityFilterChain that defines who can reach what:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").hasRole("USER")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
Request as the USER account:
curl -u alice:password http://localhost:8080/api/orders
Output:
HTTP/1.1 200 OK
Request the admin path as a non-admin:
curl -u alice:password http://localhost:8080/api/admin/stats
Output:
HTTP/1.1 403 Forbidden
Using it in tests
In-memory users shine in @SpringBootTest/@WebMvcTest. Combine with @WithMockUser to skip the password round-trip entirely:
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mvc;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanSeeStats() throws Exception {
mvc.perform(get("/api/admin/stats"))
.andExpect(status().isOk());
}
}
Adding users at runtime
InMemoryUserDetailsManager implements UserDetailsManager, so you can create, update, and delete users while the app runs (still in memory only):
manager.createUser(User.builder()
.username("carol")
.password(encoder.encode("temp"))
.roles("USER")
.build());
Tip: Because the store lives in heap memory, every restart resets it and it does not work across multiple instances. Treat it strictly as non-persistent.