The Authentication Flow

From Credentials to Authenticated User

1. BasicAuthenticationFilter reads "Authorization: Basic YXNoYTphc2hhMTIz"
          |
          v
2. creates UsernamePasswordAuthenticationToken("asha", "asha123")   authenticated = false
          |
          v
3. AuthenticationManager (ProviderManager)
          |  asks each AuthenticationProvider: supports(this token type)?
          v
4. DaoAuthenticationProvider
          |-- UserDetailsService.loadUserByUsername("asha")   -> UserDetails (hash, roles)
          |-- PasswordEncoder.matches("asha123", "$2a$10$...")  -> true
          v
5. returns UsernamePasswordAuthenticationToken(userDetails, null, [ROLE_CUSTOMER])  authenticated = true
          |
          v
6. filter stores it: SecurityContextHolder.getContext().setAuthentication(auth)
          |
          v
7. AuthorizationFilter checks the rules -> controller runs

   Wrong password -> BadCredentialsException -> 401

Manager and Providers

AuthenticationManager is a one-method interface: Authentication authenticate(Authentication). Its usual implementation, ProviderManager, holds a list of AuthenticationProviders and tries each one that supports the token type. This is how one app can accept passwords, JWTs and LDAP at the same time.

Example

A toy ProviderManager with two providers:

import java.util.*;

public class Main {
    sealed interface Token permits Password, ApiKey {}
    record Password(String username, String password) implements Token {}
    record ApiKey(String key) implements Token {}
    record AuthResult(String principal, List<String> authorities) {}

    interface AuthenticationProvider {
        boolean supports(Token t);
        AuthResult authenticate(Token t);      // throws on bad credentials
    }

    static class DaoProvider implements AuthenticationProvider {
        public boolean supports(Token t) { return t instanceof Password; }
        public AuthResult authenticate(Token t) {
            Password p = (Password) t;
            if (p.username().equals("asha") && p.password().equals("asha123"))
                return new AuthResult("asha", List.of("ROLE_CUSTOMER"));
            throw new IllegalStateException("BadCredentialsException: Bad credentials");
        }
    }

    static class ApiKeyProvider implements AuthenticationProvider {
        public boolean supports(Token t) { return t instanceof ApiKey; }
        public AuthResult authenticate(Token t) {
            if (((ApiKey) t).key().equals("partner-delhivery-key")) return new AuthResult("delhivery", List.of("ROLE_PARTNER"));
            throw new IllegalStateException("BadCredentialsException: Invalid API key");
        }
    }

    static AuthResult providerManager(List<AuthenticationProvider> providers, Token token) {
        for (AuthenticationProvider p : providers)
            if (p.supports(token)) return p.authenticate(token);
        throw new IllegalStateException("ProviderNotFoundException");
    }

    public static void main(String[] args) {
        List<AuthenticationProvider> providers = List.of(new DaoProvider(), new ApiKeyProvider());
        Token[] attempts = { new Password("asha", "asha123"), new ApiKey("partner-delhivery-key"), new Password("asha", "nope") };
        for (Token t : attempts) {
            try {
                System.out.println(t + " -> " + providerManager(providers, t));
            } catch (IllegalStateException e) {
                System.out.println(t + " -> 401 " + e.getMessage());
            }
        }
    }
}

Output

Password[username=asha, password=asha123] -> AuthResult[principal=asha, authorities=[ROLE_CUSTOMER]]
ApiKey[key=partner-delhivery-key] -> AuthResult[principal=delhivery, authorities=[ROLE_PARTNER]]
Password[username=asha, password=nope] -> 401 BadCredentialsException: Bad credentials

Spring Configures This for You

When you declare a UserDetailsService bean and a PasswordEncoder bean, Spring Security creates a DaoAuthenticationProvider and a ProviderManager automatically. You only build them yourself in special cases, such as a custom login endpoint:

@Bean
AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);   // Spring Security 6.3+
    provider.setPasswordEncoder(passwordEncoder);
    return new ProviderManager(provider);
}

Note: A custom AuthenticationProvider (OTP login, partner API keys) is covered in the advance level.