LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2822 min read

Building a Login/Authentication Flow

Build a database-backed authentication flow with UserDetailsService, a registration endpoint, and a login endpoint that issues a token.

Introduction

The previous lesson covered the basics of locking down endpoints with Spring Security. This lesson builds a complete, database-backed authentication flow on top of that foundation: users register with a username and password, log in to receive a token, and use that token on subsequent requests. This is the pattern behind most real-world Spring Boot APIs.

What You Will Learn
  • The pieces that make up a login/authentication flow.
  • How to model a User entity for authentication.
  • How to implement a custom UserDetailsService backed by the database.
  • How to build registration and login endpoints.
  • How a token is used to protect subsequent requests.

The Pieces of an Auth Flow

A database-backed authentication flow has a handful of moving parts that fit together in a predictable way.

PieceResponsibility
User entityStores username and a hashed password
UserDetailsServiceLoads a User from the database for Spring Security to authenticate against
Registration endpointCreates a new user with an encoded password
Login endpointVerifies credentials and issues a token
SecurityFilterChainValidates the token on every subsequent request

The User Entity

Implementing UserDetails directly on the entity is a common, convenient approach - it lets Spring Security work with your own User class with no separate adapter type.

@Entity
@Table(name = "app_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getUsername() { return username; }
@Override
public String getPassword() { return password; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
// getters and setters for id, username, password
}

Custom UserDetailsService

UserDetailsService has a single method, loadUserByUsername, that Spring Security calls during authentication. Implementing it against your repository is all that is needed to authenticate against your own database.

@Service
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AppUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No user: " + username));
}
}

Registration Endpoint

Registration creates a new User row with the password encoded, never stored in plain text.

public record RegisterRequest(@NotBlank String username, @NotBlank @Size(min = 8) String password) {}
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AuthController(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/register")
public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request) {
User user = new User();
user.setUsername(request.username());
user.setPassword(passwordEncoder.encode(request.password()));
userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
Request

Click Run to see what this code prints.

Response

Click Run to see what this code prints.

Login Endpoint

The login endpoint authenticates the submitted credentials with AuthenticationManager, then issues a token. The example below issues a JWT signed with a server-side secret; a full JWT implementation is a topic of its own, so this shows the shape of the flow.

public record LoginRequest(String username, String password) {}
public record LoginResponse(String token) {}
@RestController
@RequestMapping("/api/auth")
public class LoginController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
public LoginController(AuthenticationManager authenticationManager, JwtService jwtService) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
}
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password())
);
String token = jwtService.generateToken(authentication.getName());
return new LoginResponse(token);
}
}
Response

Click Run to see what this code prints.

Protecting Endpoints

A JwtAuthenticationFilter (registered before UsernamePasswordAuthenticationFilter in the SecurityFilterChain) reads the Authorization header on every request, validates the token, and sets the authenticated user on the SecurityContext so downstream controllers see a properly authenticated request.

Authenticated Request

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Comparing raw passwords with equals() instead of letting PasswordEncoder and AuthenticationManager do the comparison.
  • Returning "user not found" versus "wrong password" as different error messages, which leaks which usernames exist.
  • Storing a JWT signing secret in source control instead of an environment variable or secret manager.
  • Forgetting to set a token expiration, leaving tokens valid indefinitely.
  • Not validating registration input, allowing empty usernames or trivially short passwords.

Best Practices

  • Use one generic error message for failed login, regardless of whether the username or password was wrong.
  • Set a reasonable token expiration and support refreshing it rather than issuing tokens that never expire.
  • Validate registration input with Bean Validation, as covered in the validation lesson.
  • Keep signing secrets and other credentials in environment-specific configuration, never hard-coded.
  • Log authentication failures for security monitoring, without logging the submitted password.

Frequently Asked Questions

No. This lesson shows the underlying mechanics, but for production use many teams rely on Spring Security's OAuth2 Resource Server support or a managed identity provider (Auth0, Okta, Keycloak) instead of hand-rolling token issuance.

That depends on the client type. Browser-based single-page apps commonly face tradeoffs between local storage (vulnerable to XSS) and httpOnly cookies (which reintroduce CSRF considerations) - a topic worth researching for your specific frontend.

It delegates to one or more AuthenticationProvider beans - by default, one that uses your UserDetailsService and PasswordEncoder - to verify the submitted username and password before returning an authenticated Authentication object.

Key Takeaways

  • A database-backed auth flow needs a User entity, a UserDetailsService, and encoded password storage.
  • Registration encodes the password before saving; login verifies it through AuthenticationManager.
  • A successful login issues a token that the client sends on subsequent requests.
  • A filter validates that token on every request and populates the SecurityContext.
  • Use one generic failure message for login errors to avoid leaking which usernames exist.

Summary

A complete authentication flow combines a User entity, a custom UserDetailsService, encoded password storage, and a token issued at login that protects every subsequent request. With this foundation in place, the next lesson looks at the other direction - calling external APIs from within your own Spring Boot application.

Next Lesson →

Consuming External APIs