Spring Boot Tutorial
Authentication with Users and Roles
Spring Security needs to know two things to log someone in: how to find a user by username, and how to check their password. The first is the job of a UserDetailsService; the second is the job of a PasswordEncoder. Swap either one and the rest of the framework keeps working unchanged.
In this lesson you will start with in-memory users for prototypes, move to the built-in JDBC user store, and finish with the approach most real applications use: your own JPA User entity loaded through a custom UserDetailsService, plus a registration endpoint that stores hashed passwords.
UserDetails, GrantedAuthority, Roles and Authorities
UserDetails is Spring Security's view of a user: a username, a hashed password, a collection of GrantedAuthority objects, and flags for enabled/locked/expired. A GrantedAuthority is just a string permission such as orders:read. A role is an authority with the ROLE_ prefix — hasRole("ADMIN") checks for the authority ROLE_ADMIN.
Use roles for coarse groups of users (USER, ADMIN) and fine-grained authorities for specific permissions (orders:refund). Many applications store roles in the database and expand them into authorities when the user is loaded.
In-Memory Users for Prototypes and Tests
InMemoryUserDetailsManager keeps users in a map. It is perfect for demos, internal tools and tests, and completely unsuitable for real users because accounts disappear on restart and cannot be managed at runtime. Declaring a UserDetailsService bean also stops Spring Boot from generating its random default password.
JdbcUserDetailsManager
JdbcUserDetailsManager reads users from two tables, users and authorities, with a schema Spring Security ships in org/springframework/security/core/userdetails/jdbc/users.ddl. It also implements create, update, delete and change-password operations. It is useful when you want a quick database-backed store and do not need your own user model.
A Custom UserDetailsService Backed by JPA
Most applications already have a User entity with fields like email, full name and signup date. You implement UserDetailsService.loadUserByUsername to look the user up with a repository and convert it into a UserDetails. Spring Boot detects your single UserDetailsService bean and a PasswordEncoder bean, and automatically builds the DaoAuthenticationProvider for you.
Always throw UsernameNotFoundException when the user does not exist. Spring Security deliberately converts this into a generic "Bad credentials" error so attackers cannot discover which usernames exist.
Registering Users Safely
A registration endpoint must validate input, reject duplicate usernames, hash the password with the PasswordEncoder before saving, and assign a default role. Never accept roles from the request body; a user who can send "roles":["ADMIN"] should not become an administrator.
Custom Login Endpoints for APIs
Form login and HTTP Basic cover many cases, but single-page apps and mobile clients often POST JSON to /api/auth/login. You can inject the AuthenticationManager and call authenticate() yourself. On success you either save the context into the session (stateful) or issue a token (stateless; see the JWT lesson).
Examples
In-memory users with roles (prototype only)
@Configuration
public class InMemoryUsersConfig {
@Bean
UserDetailsService users(PasswordEncoder encoder) {
UserDetails asha = User.withUsername("asha")
.password(encoder.encode("user123"))
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("admin123"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(asha, admin);
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
curl -u asha:user123 http://localhost:8080/api/me -> 200 {"username":"asha","authorities":["ROLE_USER"]}
curl -u asha:wrong http://localhost:8080/api/me -> 401 Unauthorized
JPA entities for users and roles
@Entity
@Table(name = "app_users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String passwordHash;
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "app_user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
protected AppUser() {}
public AppUser(String email, String passwordHash, Set<String> roles) {
this.email = email;
this.passwordHash = passwordHash;
this.roles = roles;
}
// getters omitted for brevity
}
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByEmailIgnoreCase(String email);
boolean existsByEmailIgnoreCase(String email);
}
Hibernate: create table app_users (id bigint generated by default as identity, email varchar(255) not null unique, enabled boolean not null, password_hash varchar(255) not null, primary key (id))
Hibernate: create table app_user_roles (user_id bigint not null, role varchar(255))
Custom UserDetailsService that loads users from the database
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
private final AppUserRepository users;
public DatabaseUserDetailsService(AppUserRepository users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
AppUser user = users.findByEmailIgnoreCase(email)
.orElseThrow(() -> new UsernameNotFoundException("No user " + email));
return User.withUsername(user.getEmail())
.password(user.getPasswordHash())
.disabled(!user.isEnabled())
.roles(user.getRoles().toArray(String[]::new))
.build();
}
}
curl -u asha@webnest.in:user123 http://localhost:8080/api/me
{"username":"asha@webnest.in","authorities":["ROLE_USER"]}
Registration and JSON login endpoints
public record RegisterRequest(@Email @NotBlank String email,
@NotBlank @Size(min = 12, max = 128) String password) {}
public record LoginRequest(@NotBlank String email, @NotBlank String password) {}
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AppUserRepository users;
private final PasswordEncoder encoder;
private final AuthenticationManager authManager;
public AuthController(AppUserRepository users, PasswordEncoder encoder,
AuthenticationManager authManager) {
this.users = users;
this.encoder = encoder;
this.authManager = authManager;
}
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public void register(@Valid @RequestBody RegisterRequest req) {
if (users.existsByEmailIgnoreCase(req.email())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Email already registered");
}
// Role is decided by the server, never by the client
users.save(new AppUser(req.email(), encoder.encode(req.password()), Set.of("USER")));
}
@PostMapping("/login")
public Map<String, Object> login(@Valid @RequestBody LoginRequest req) {
Authentication auth = authManager.authenticate(
UsernamePasswordAuthenticationToken.unauthenticated(req.email(), req.password()));
return Map.of("user", auth.getName(), "authorities",
auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList());
}
}
// Expose the AuthenticationManager Spring Boot builds from your UserDetailsService
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
POST /api/auth/register {"email":"ravi@webnest.in","password":"correct-horse-battery"} -> 201 Created
POST /api/auth/register (same email again) -> 409 Conflict
POST /api/auth/login {"email":"ravi@webnest.in","password":"correct-horse-battery"} -> 200 {"user":"ravi@webnest.in","authorities":["ROLE_USER"]}
POST /api/auth/login (wrong password) -> 401 Unauthorized
Common Mistakes
- Storing the raw password or calling encoder.encode() on every login instead of letting DaoAuthenticationProvider call encoder.matches().
- Revealing whether an account exists with messages like "user not found" versus "wrong password" — always return the same generic error.
- Accepting roles from the registration request body, which lets any user make themselves an administrator.
- Defining two UserDetailsService beans; Spring Boot then cannot choose one and does not auto-configure the authentication provider.
- Forgetting the ROLE_ prefix difference: roles("ADMIN") creates ROLE_ADMIN, while authorities("ADMIN") creates plain ADMIN, which hasRole("ADMIN") will not match.
Key Points to Remember
- UserDetailsService finds users; PasswordEncoder checks passwords; DaoAuthenticationProvider combines them.
- Roles are authorities prefixed with ROLE_; use authorities for fine-grained permissions.
- InMemoryUserDetailsManager is for prototypes and tests; real apps load users from a database.
- Registration must validate input, reject duplicates, hash passwords and assign roles on the server.
- Inject AuthenticationManager when you need a custom JSON login endpoint.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.