Spring BootAuthentication
Spring Boot

Authentication

Authentication is the process of verifying who a user is. Spring Security supports multiple authentication mechanisms — form login, HTTP Basic, JWT bearer tokens, OAuth2, and custom token schemes. The authentication pipeline is built on AuthenticationManager, AuthenticationProvider, and UserDetailsService, making each layer independently replaceable.

Authentication Architecture

Spring Security's authentication pipeline has three layers. The AuthenticationManager receives an unauthenticated Authentication token and delegates to a list of AuthenticationProviders. Each provider attempts to authenticate the token using its strategy — database lookup, LDAP, OAuth2, etc. On success, the provider returns a fully populated Authentication object that is stored in the SecurityContextHolder for the duration of the request.
Shell
# ── Authentication flow: ─────────────────────────────────────────────
#
#  Request with credentials (username/password, JWT, Basic header)
#       │
#       ▼
#  Authentication Filter      (UsernamePasswordAuthenticationFilter,
#       │                       BasicAuthenticationFilter,
#       │                       custom JWT filter, etc.)
#       │  Creates an unauthenticated Authentication token
#       ▼
#  AuthenticationManager      (ProviderManager — delegates to providers)
#       │
#       ▼
#  AuthenticationProvider     (DaoAuthenticationProvider — DB lookup)
#       │                      (LdapAuthenticationProvider — LDAP)
#       │                      (JwtAuthenticationProvider — custom)
#       │  Calls UserDetailsService.loadUserByUsername(username)
#       │  Verifies password with PasswordEncoder.matches()
#       ▼
#  UserDetails loaded, credentials verified
#       │
#       ▼
#  Authenticated Authentication token (principal + authorities)
#       │
#       ▼
#  SecurityContextHolder.getContext().setAuthentication(auth)
#       │
#       ▼
#  Request continues to controller

// ── AuthenticationManager — entry point: ─────────────────────────────
AuthenticationManager manager = authenticationConfiguration.getAuthenticationManager();

// Authenticate manually (e.g. in a login endpoint):
Authentication token =
    new UsernamePasswordAuthenticationToken(username, password);
Authentication authenticated = manager.authenticate(token);
// Throws AuthenticationException if credentials are wrong.
// Returns fully populated Authentication on success.

Form Login Authentication

Form login is the standard authentication mechanism for server-side rendered web applications. Spring Security's UsernamePasswordAuthenticationFilter intercepts POST /login, extracts credentials from the form, and delegates to the AuthenticationManager.
Java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class FormLoginSecurityConfig {

    private final UserDetailsServiceImpl userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/register", "/css/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")                    // custom login page GET
                .loginProcessingUrl("/login")           // POST target (default)
                .usernameParameter("username")          // form field name
                .passwordParameter("password")          // form field name
                .defaultSuccessUrl("/dashboard", true)  // redirect after success
                .failureUrl("/login?error=true")        // redirect on failure
                .failureHandler((req, res, ex) -> {     // custom failure handling
                    req.setAttribute("errorMessage", ex.getMessage());
                    req.getRequestDispatcher("/login").forward(req, res);
                })
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout=true")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
            );
        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }
}

// ── Login controller — renders the login form: ────────────────────────
@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginPage(
            @RequestParam(required = false) String error,
            @RequestParam(required = false) String logout,
            Model model) {
        if (error != null)  model.addAttribute("error", "Invalid credentials");
        if (logout != null) model.addAttribute("message", "Logged out successfully");
        return "login";
    }
}

HTTP Basic Authentication

HTTP Basic sends credentials as a Base64-encoded Authorization header on every request. It is simple and stateless — appropriate for machine-to-machine REST API calls and internal services. Credentials must always be transmitted over HTTPS since Base64 is not encryption.
Java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .httpBasic(basic -> basic
            .realmName("MyApp API")             // shown in browser challenge dialog
            .authenticationEntryPoint(          // custom 401 response
                (request, response, authException) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    response.getWriter().write(
                        "{"error": "Unauthorized", " +
                        ""message": "" + authException.getMessage() + ""}");
                })
        )
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .csrf(csrf -> csrf.disable());
    return http.build();
}

# ── Client request with HTTP Basic: ──────────────────────────────────
# Authorization: Basic dXNlcjpwYXNzd29yZA==
# (Base64 of "user:password")

# Using curl:
curl -u username:password https://api.example.com/api/users

# Using HTTPie:
http -a username:password GET https://api.example.com/api/users

JWT Authentication

JWT (JSON Web Token) is the standard authentication mechanism for stateless REST APIs. The client authenticates once with credentials, receives a signed JWT, and presents the JWT on subsequent requests. The server validates the signature without database lookup — the token is self-contained.
XML
<!-- pom.xml — JWT library: -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>

// ── JWT utility service: ──────────────────────────────────────────────
@Service
public class JwtService {

    @Value("${app.jwt.secret}")
    private String secretKey;

    @Value("${app.jwt.expiration:86400}")
    private long expirationSeconds;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
    }

    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis()
                + expirationSeconds * 1000))
            .claim("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList())
            .signWith(getSigningKey())
            .compact();
    }

    public String extractUsername(String token) {
        return extractClaims(token).getSubject();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        String username = extractUsername(token);
        return username.equals(userDetails.getUsername())
            && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractClaims(token).getExpiration().before(new Date());
    }

    private Claims extractClaims(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
}

// ── JWT filter — validates token on every request: ────────────────────
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7);
        String username;

        try {
            username = jwtService.extractUsername(token);
        } catch (JwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{"error": "Invalid token"}");
            return;
        }

        if (username != null &&
                SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

// ── Security config with JWT filter: ─────────────────────────────────
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
        JwtAuthenticationFilter jwtFilter) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated())
        .addFilterBefore(jwtFilter,
            UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

Login Endpoint

A REST API needs a login endpoint that accepts credentials, authenticates them, and returns a JWT. This endpoint is the entry point for all authenticated API clients.
Java
// ── Request/Response DTOs: ────────────────────────────────────────────
public record LoginRequest(
    @NotBlank String username,
    @NotBlank String password
) { }

public record LoginResponse(
    String token,
    String username,
    List<String> roles,
    long expiresIn
) { }

// ── Auth controller: ──────────────────────────────────────────────────
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(
            @RequestBody @Valid LoginRequest request) {
        try {
            // Authenticate — throws AuthenticationException on failure:
            Authentication auth = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.username(), request.password()));

            UserDetails userDetails = (UserDetails) auth.getPrincipal();
            String token = jwtService.generateToken(userDetails);

            List<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .toList();

            return ResponseEntity.ok(new LoginResponse(
                token,
                userDetails.getUsername(),
                roles,
                86400L
            ));

        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(null);
        }
    }

    @PostMapping("/register")
    public ResponseEntity<UserResponse> register(
            @RequestBody @Valid RegisterRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(userService.register(request));
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(
            @RequestHeader("Authorization") String authHeader) {
        // For stateless JWT: client discards the token.
        // For token blacklisting: add token to a Redis blacklist here.
        return ResponseEntity.noContent().build();
    }
}

# ── Example request/response: ────────────────────────────────────────
# POST /api/auth/login
# { "username": "alice", "password": "secret123" }
#
# 200 OK:
# {
#   "token": "eyJhbGciOiJIUzI1NiJ9...",
#   "username": "alice",
#   "roles": ["ROLE_USER"],
#   "expiresIn": 86400
# }
#
# Subsequent requests:
# Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Custom AuthenticationProvider

Implement AuthenticationProvider when the standard username/password flow is insufficient — API key authentication, OTP verification, multi-factor authentication, or any custom credential scheme.
Java
// ── API key authentication provider: ─────────────────────────────────

// Custom authentication token (carries the API key):
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {

    private final String apiKey;

    public ApiKeyAuthenticationToken(String apiKey) {
        super(null);          // no authorities yet — unauthenticated
        this.apiKey = apiKey;
        setAuthenticated(false);
    }

    public ApiKeyAuthenticationToken(String apiKey,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);   // authenticated — has authorities
        this.apiKey = apiKey;
        setAuthenticated(true);
    }

    @Override public Object getCredentials() { return apiKey; }
    @Override public Object getPrincipal()   { return apiKey; }
}

// Custom provider that validates API keys:
@Component
@RequiredArgsConstructor
public class ApiKeyAuthenticationProvider implements AuthenticationProvider {

    private final ApiKeyRepository apiKeyRepository;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        String apiKey = (String) authentication.getCredentials();

        ApiKeyEntity entity = apiKeyRepository.findByKeyValue(apiKey)
            .orElseThrow(() -> new BadCredentialsException("Invalid API key"));

        if (!entity.isActive()) {
            throw new DisabledException("API key is disabled");
        }

        List<GrantedAuthority> authorities = entity.getScopes().stream()
            .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
            .toList();

        return new ApiKeyAuthenticationToken(apiKey, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

// API key filter — extracts key from X-API-Key header:
@Component
@RequiredArgsConstructor
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {

    private final AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        String apiKey = request.getHeader("X-API-Key");
        if (apiKey != null && SecurityContextHolder.getContext()
                .getAuthentication() == null) {
            try {
                Authentication auth = authenticationManager.authenticate(
                    new ApiKeyAuthenticationToken(apiKey));
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (AuthenticationException e) {
                SecurityContextHolder.clearContext();
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}