Skip to content

Spring Boot · Security

How Authentication Works in Spring Security

5 min read Updated 2026-06-26 Share:

Practice Authentication interview questions

The cast of characters

Authentication in Spring Security looks tangled until you learn the four roles, then it's a clean assembly line. A filter captures credentials, an AuthenticationManager coordinates, an AuthenticationProvider verifies, and a UserDetailsService loads the user:

filter → AuthenticationManager → AuthenticationProvider
       → UserDetailsService.loadUserByUsername()
       → PasswordEncoder.matches()
       → authenticated Authentication → SecurityContext

The output is an authenticated Authentication placed in the SecurityContext, available for the rest of the request. Knowing each station's job is what lets you answer "how does login actually work" without hand-waving.

UserDetailsService: bridging your database

The default in-memory user is scaffolding. Real apps implement UserDetailsService — a single method that loads a user by username — to bridge their own table to Spring Security:

@Service
class JpaUserDetailsService implements UserDetailsService {
    private final UserRepository users;
    public UserDetails loadUserByUsername(String username) {
        AppUser u = users.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(username));
        return User.withUsername(u.getUsername())
            .password(u.getPasswordHash())          // the STORED hash
            .authorities(u.getRoles().toArray(String[]::new))
            .build();
    }
}

Notice you return the stored hash, not a plaintext password. You never decode anything — Spring compares the submitted password against this hash with the PasswordEncoder. Defining this bean replaces the generated console user entirely.

UserDetails: the contract

Spring only ever talks to the UserDetails interface, which decouples it from your entity. It exposes the username, the hashed password, the authorities, and four account-status flags:

public interface UserDetails {
    String getUsername();
    String getPassword();
    Collection<? extends GrantedAuthority> getAuthorities();
    boolean isEnabled();
    boolean isAccountNonLocked();
    // isAccountNonExpired, isCredentialsNonExpired
}

You can return the built-in User or make your own entity implement the interface. The status flags are quietly powerful — they let Spring reject disabled or locked accounts after the password matches, with no extra controller logic.

Manager and providers

The AuthenticationManager is the entry point; the standard ProviderManager holds a list of AuthenticationProviders and tries each until one handles the token. For username/password, that's DaoAuthenticationProvider:

@Bean
AuthenticationManager authManager(UserDetailsService uds, PasswordEncoder pe) {
    var provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(uds);
    provider.setPasswordEncoder(pe);
    return new ProviderManager(provider);
}

DaoAuthenticationProvider does the real work: load the user, run passwordEncoder.matches(), check the status flags, and produce an authenticated token. In Boot you often don't build it by hand — defining a UserDetailsService and PasswordEncoder bean is enough for auto-configuration to wire one up. The multi-provider design is how one manager can support password, LDAP, and token auth together.

Form login vs HTTP Basic

Two built-in mechanisms cover most cases. Form login shows an HTML page, posts credentials, and stores auth in a session — for browser apps. HTTP Basic sends the credentials in a header on every request — for APIs and tooling:

http.formLogin(withDefaults());   // login page + session
// or
http.httpBasic(withDefaults());   // Authorization: Basic ... on every request

Basic must run over HTTPS — base64 is encoding, not encryption — and has no real logout. Form login gives you a UX and a session lifecycle. APIs that want neither usually move to a token scheme.

Reading the current user

In a controller, don't reach into SecurityContextHolder. Inject the principal:

@GetMapping("/me")
String me(@AuthenticationPrincipal UserDetails user) {
    return user.getUsername();
}

@AuthenticationPrincipal can even bind your custom principal type directly, and an Authentication parameter gives you the authorities. Spring resolves these per request from the context, so they always reflect the current user.

A custom AuthenticationProvider

When credentials don't fit username/password — an API key, an external code — implement AuthenticationProvider. authenticate() verifies; supports() declares the token type:

@Component
class ApiKeyAuthProvider implements AuthenticationProvider {
    public Authentication authenticate(Authentication a) {
        String key = (String) a.getCredentials();
        if (!keyService.isValid(key)) throw new BadCredentialsException("bad key");
        return new ApiKeyAuthenticationToken(key, keyService.authorities(key));
    }
    public boolean supports(Class<?> type) {
        return ApiKeyAuthenticationToken.class.isAssignableFrom(type);
    }
}

Return an authenticated token on success, throw an AuthenticationException on failure, and register it with the manager.

The deliberate vagueness of "Bad credentials"

A detail that confuses people: even an unknown username surfaces as BadCredentialsException, not "user not found." That's on purpose — it prevents username enumeration. Spring also runs the password check against a dummy hash for unknown users so the response timing is similar, defeating timing attacks. Keep this default on in production; a helpful "no such user" message is a gift to attackers.

Account status, events, and logout

Because UserDetails carries status flags, lockout and deactivation are declarative — a correct password still can't log in a disabled account. Login outcomes are also published as events you can audit without touching the auth flow:

@EventListener
void onFailure(AbstractAuthenticationFailureEvent e) {
    log.warn("Login failed for {}", e.getAuthentication().getName());
}

For session apps, /logout invalidates the session, clears the context, and drops the cookie. For stateless token apps there's nothing server-side to invalidate — "logout" means the client discards the token, optionally backed by a server-side deny-list until it expires.

Recap

Authentication flows filter → manager → provider → user store, producing an authenticated token in the SecurityContext. Implement UserDetailsService to load users (returning the stored hash) and let DaoAuthenticationProvider + PasswordEncoder verify them. Pick form login for browsers and HTTP Basic for APIs; read the user via @AuthenticationPrincipal; add a custom AuthenticationProvider for non-standard credentials; lean on the UserDetails status flags and authentication events for lockout and auditing; and remember that "Bad credentials" is intentionally vague to stop username enumeration.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel