Managing Users: UserDetailsService & UserDetailsManager
Every authentication flow eventually needs to answer one question: who is this user, really? This chapter is UserDetailsService and UserDetailsManager — the mechanism Spring Security uses to look that up.
Learning objectives
- Beginner: Explain what UserDetailsService's loadUserByUsername() is responsible for, and what it is NOT responsible for.
- Intermediate: Choose between InMemoryUserDetailsManager, JdbcUserDetailsManager, and a custom UserDetailsService for a given project's stage.
- Advanced: Implement a production-shaped custom UserDetailsService backed by your own user table and entity.
🌱 BEGINNER STORY
Picture a gym with a membership office. When you walk in claiming to be 'Rahul', the front desk has to look YOU up in their records — is there a member named Rahul, what's his membership card number, is his membership active or frozen, and what tier does he have (basic/premium)? Spring Security's UserDetailsService is exactly this front-desk lookup: given a username, go fetch that user's full profile — password hash, enabled/locked/expired flags, and granted roles — from wherever that data lives (in-memory list, MySQL, LDAP, etc.). It doesn't check the password itself; it just returns the record so something else (the AuthenticationProvider) can compare it.
UserDetailsService: A read-only interface with a single method — loadUserByUsername(String username) returning a UserDetails object or throwing UsernameNotFoundException. This is the minimum contract Spring Security needs to authenticate someone.
UserDetailsManager: Extends UserDetailsService and adds full CRUD-style operations: createUser(), updateUser(), deleteUser(), changePassword(), userExists(). Use this when your application also needs to manage (not just read) user accounts.
Spring Security ships two ready-made UserDetailsManager implementations: InMemoryUserDetailsManager (users defined in configuration/code — great for demos, tests, or extremely small internal tools) and JdbcUserDetailsManager (users stored in a relational database, using a predefined default schema of 'users' and 'authorities' tables).
@Bean
public UserDetailsService userDetailsService() { UserDetails admin = User.withUsername("admin") .password(passwordEncoder().encode("admin@123")) .authorities("ADMIN") .build(); UserDetails user = User.withUsername("john") .password(passwordEncoder().encode("john@123")) .authorities("READ") .build(); return new InMemoryUserDetailsManager(admin, user); }
This is perfect for prototypes and learning, but it has an obvious real-world limitation: user data disappears on every restart, and there's no way for actual end-users to self-register — which is why almost every production system moves to a database-backed approach.
Spring Security provides a default SQL schema (users, authorities tables) that JdbcUserDetailsManager expects out of the box:
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY, password VARCHAR(500) NOT NULL, enabled BOOLEAN NOT NULL ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users (username) );
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); }
This works, but real applications almost always have their own richer 'customers' or 'users' table (with email, phone, address, KYC data, etc.) that doesn't match Spring's default schema — which is exactly why most production teams write a CUSTOM UserDetailsService instead.
This is the pattern you will use in almost every real project: your own JPA entity + repository, plus a UserDetailsService implementation that adapts your entity into Spring Security's UserDetails contract.
@Entity
@Table(name = "customer") public class Customer { @Id @GeneratedValue private Long id; private String email; private String pwd; private String role; // e.g., "USER", "ADMIN" // getters/setters omitted } public interface CustomerRepository extends JpaRepository<Customer, Long> { Optional findByEmail(String email); } @Service public class EazyBankUserDetailsService implements UserDetailsService { private final CustomerRepository customerRepository; public EazyBankUserDetailsService(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Customer customer = customerRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException( "User not found with email: " + email)); return new org.springframework.security.core.userdetails.User( customer.getEmail(), customer.getPwd(), Collections.singletonList(new SimpleGrantedAuthority(customer.getRole()))); } }
UserDetails is the interface that represents 'a fetched user record ready for authentication'. Its most important methods (all boolean, all defaulting to true in most implementations) are:
-
isEnabled() — false means the account has been deliberately disabled (e.g., admin deactivated it).
-
isAccountNonLocked() — false means the account is temporarily locked (e.g., too many failed login attempts).
-
isAccountNonExpired() — false means the account itself has expired (e.g., a contractor's access that was time-boxed).
-
isCredentialsNonExpired() — false means the PASSWORD has expired and must be changed (a common corporate policy: 'change your password every 90 days').
If any of these return false during authentication, DaoAuthenticationProvider throws a specific exception (DisabledException, LockedException, AccountExpiredException, CredentialsExpiredException respectively) — but NOT all four fire at the same point. isEnabled(), isAccountNonLocked(), and isAccountNonExpired() are pre-authentication checks, run BEFORE the password is even compared — a deliberate security design so a locked account can't be brute-forced even with the correct password. isCredentialsNonExpired() is different: it's a post-authentication check, run only AFTER the password has already been verified correct — which makes sense once you think about it, since "your password is correct but has expired, please change it" is a message you can only safely show once you've confirmed the password actually was correct.
@PostMapping("/register")
public ResponseEntity register(@RequestBody CustomerDto dto) { Customer customer = new Customer(); customer.setEmail(dto.getEmail()); // NEVER store the raw password - always encode first! customer.setPwd(passwordEncoder.encode(dto.getPassword())); customer.setRole("USER"); customerRepository.save(customer); return ResponseEntity.ok("User registered successfully"); }
⚠ The #1 real-world mistake here
Forgetting passwordEncoder.encode() during registration is one of the most common production incidents. If you encode passwords during login-verification but forget to encode them during SIGN-UP, every new user's password gets stored as plain text, and login will actually FAIL for them (because PasswordEncoder.matches() compares a raw string against what it thinks is an encoded hash). Always encode at the single point of entry: registration/password-change, never anywhere else.
⚠ Things that bite teams in production
Username case-sensitivity: if your DB lookup is case-sensitive but users sometimes type 'John@x.com' vs 'john@x.com', authentication randomly fails — normalize (lowercase) the identifier consistently on both registration and login. Loading full user + roles from the DB on EVERY request is expensive — teams often cache UserDetails briefly, or move to token-based auth (JWT/OAuth2) precisely so the DB isn't hit per-request after initial login. Custom UserDetailsService implementations often forget to wrap authorities correctly — passing a raw String list instead of GrantedAuthority objects causes a compile error or, if bypassed via reflection, silent authorization failures. JdbcUserDetailsManager's default schema is intentionally minimal — trying to force your real business 'users' table to conform to it (rather than writing a custom UserDetailsService) usually creates more friction than it saves.
Want a visual for this concept?
Generate a diagram tailored to “Managing Users: UserDetailsService & UserDetailsManager” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →