Spring Security, Part II: In-Memory Authentication with UserDetailsService
Securing Your APIs with In-Memory Authentication in Spring Boot using Kotlin
Welcome back to the Spring Security Series! If you haven’t read the first tutorial, I recommend checking that out before continuing — it sets the foundation for what we’re about to explore.
In this post, we’ll dive into:
Pre-configured security setup in
Spring BootUserDetailsandUserDetailsServiceCreating an in-memory user with
InMemoryUserDetailsManager
Spring Security is highly extensible and provides default authentication mechanisms out of the box. Simply adding the security starter dependency gets you a secure setup with zero configuration.
Let’s walk through it with a basic Kotlin Spring Boot project.
🛠 Project Setup
build.gradle.kts
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.5.0"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.atomic.coding"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.security:spring-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}🧾 A Simple REST Controller
We’ll create a basic endpoint to return a list of orders:
@RestController
@RequestMapping("/api/orders")
class OrderController(
private val orderService: OrderService
) {
@GetMapping
fun orders(): List<Order> = orderService.orders()
}🔐 Pre-Configured Security
Let’s run the app and send a request to /api/orders. You’ll receive:
📛 401 Unauthorized. OOps.
That’s expected. Even without any custom configuration, Spring Security enforces basic authentication by default.
In the console, Spring logs:
Using generated security password: 98f6dd92-c74a-4b42-abc2-61a40a3bb6a3✅ Try using:
Username:
userPassword: the generated value above
Now the request will succeed, returning a 200 OK and the order list.
🧱 Under the Hood: Spring Security Components
This is a very simplified architecture of how Spring Security manages user storage.
Spring Security manages user storage through a series of application filters that intercept incoming requests, apply security checks, and retrieve user information from a storage system (e.g., a database) to enforce access control rules.
At the core of this mechanism are two important interfaces:
UserDetailsServiceUserDetails
Spring Security doesn’t dictate how you implement these — it only cares that your implementation satisfies the contract defined by these interfaces. You’re free to use a database, cache, external service, or even hardcoded users — the choice is yours based on your requirements.
UserDetailsService
UserDetailsService is an interface with a single method:
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}This method defines the contract between your user storage and Spring Security.
Whenever Spring Securityneeds to authenticate a user, it calls this method to retrieve user information. The returned object must implement the UserDetails interface.
The storage mechanism (in-memory, database, external API, etc.) doesn’t matter — what matters is that your service returns a valid UserDetails object.
UserDetails
UserDetails — an interface that defines the core user information required by Spring Security’s authentication framework. It includes methods to retrieve the username, password, and authorities (roles or permissions). It also provides status checks such as whether the account is locked, expired, or enabled.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
default boolean isAccountNonExpired() { return true; }
default boolean isAccountNonLocked() { return true; }
default boolean isCredentialsNonExpired() { return true; }
default boolean isEnabled() { return true; }
}The most frequently used methods and data are getAuthorities(), getPassword() and getUsername().
When storing UserDetails in a database, one crucial consideration is how to safely store passwords. Storing passwords in plain text is a serious security risk. To mitigate this, Spring Security uses a PasswordEncoder to hash passwords before saving them and to verify password matches during authentication.
The PasswordEncoder interface defines methods for encoding raw passwords and verifying matches:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) { return false; }
}You can implement your own PasswordEncoder, but commonly used implementations like BCryptPasswordEncoder and Argon2PasswordEncoder provide strong, battle-tested password hashing algorithms.
By simply defining a PasswordEncoder bean in your Spring context, Spring Security will automatically use it wherever password encoding or verification is needed.
For development and testing, Spring Security also offers an in-memory implementation of UserDetailsService — the InMemoryUserDetailsManager. This is useful for quickly setting up user authentication without needing a database.
For example, you can create an in-memory user with a predefined username and a password encrypted using BCryptPasswordEncoder:
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withUsername("atomic.coding")
.password(passwordEncoder().encode("qwerty"))
.authorities("read")
.build()
return InMemoryUserDetailsManager(user)
}The complete Spring Security configuration looks like this:
@Configuration
class SecurityConfig {
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean
fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
return httpSecurity
.httpBasic {} // Enables HTTP Basic authentication
.authorizeHttpRequests { requests ->
requests.anyRequest().authenticated() // All requests require authentication
}
.build()
}
@Bean
fun userDetailsService(): UserDetailsService {
val inMemoryUser = User
.withUsername("atomic.coding")
.password("\$2a\$10\$kEQxusWgs1ncnA.f.IuedeZlvtCNSu4zVT3XovHFFmPWRcaYwrlzu") // "qwerty" encoded with BCrypt
.authorities("read")
.build()
return InMemoryUserDetailsManager(inMemoryUser)
}
}Let’s break it down:
We define a
PasswordEncoderbean usingBCryptPasswordEncoder, a strong hashing algorithm.The
securityFilterChainconfigures HTTP Basic authentication and ensures all endpoints require authentication.A custom
UserDetailsServiceis set up with a single in-memory user, using a pre-encoded password (qwerty) and thereadauthority.
With this setup in place, we can now test the secured endpoint. Use tools like Postman, cURL, or any HTTP client to authenticate using the provided credentials and access the endpoint.
❌ Error case when incorrect credentials are provided
It’s also worth noting that once we provide our own UserDetailsService, Spring Security no longer creates a default user with a randomly generated password. You can confirm this by checking the application logs — you won’t see the usual auto-generated credentials printed anymore.
✅ Recap — What We Covered
✅ How Spring Security applies default configurations
✅ The role of
UserDetailsandUserDetailsService✅ Setting up an in-memory user for local development using
InMemoryUserDetailsManager✅ Registering a
PasswordEncoderfor safe password hashing
In the next part of this series, we’ll store users in a PostgreSQL database and integrate it with Spring Security by implementing a custom UserDetailsService.
Stay tuned — and if you’re enjoying the series, feel free to like, share, or comment. See you in the next tutorial!









