Course Content
Spring Security Series
0/28
Spring Security

Introduction: Why Manually Authenticate in Spring Security?

Spring Security is a powerful and highly customizable authentication and access-control framework. For many applications, its default mechanisms are more than sufficient. With a few lines of configuration, you can enable robust security using `formLogin()` for traditional web applications or `httpBasic()` for simple APIs. These defaults are fantastic because they handle the entire authentication lifecycle for you, from rendering a login page to validating credentials and managing the user’s session.

However, the modern application landscape often demands more flexibility. What if you’re building a single-page application (SPA) with a stateless RESTful backend? What if you need to implement a multi-step login process, like two-factor authentication (2FA)? Or perhaps you need to authenticate a user against an external service via an API key. In these scenarios, the standard, automated approach falls short. This is where manual authentication becomes not just a feature, but a necessity. Manually triggering the authentication process gives you precise control, allowing you to build custom, sophisticated security workflows tailored exactly to your application’s needs.

Essential scenarios for manual authentication include:

  • RESTful APIs: Clients, like mobile apps or JavaScript frontends, typically send credentials in a JSON payload to a custom endpoint like `/api/login`. There’s no HTML form to intercept, so you must handle the request body and trigger authentication programmatically.
  • Multi-Factor Authentication (MFA/2FA): A typical 2FA flow involves first validating a password and then, in a separate step, validating a one-time password (OTP). This requires manual control to partially authenticate the user after the first step before fully establishing their session after the second.
  • Third-Party Integration: When integrating with social logins (e.g., “Login with Google”) or proprietary identity providers, you receive a token or code from the external system. Your application must then manually process this token to authenticate the user and create a session within your own security context.
  • Custom Authentication Schemes: If you need to authenticate users based on API keys, magic links sent via email, or biometric data, you will have to build a custom flow that manually invokes Spring Security’s authentication machinery.

Core Components of Spring Security Authentication

Before we dive into the code, let’s understand the key players in the Spring Security authentication process. Grasping these concepts is crucial for implementing a robust manual authentication solution. Think of them as the gears in a complex machine; knowing what each one does allows you to assemble them correctly.

  • Authentication: An interface representing the token for an authentication request or for an authenticated principal. When a user submits their credentials, you package them into an `Authentication` object (e.g., `UsernamePasswordAuthenticationToken`). At this point, it’s “unauthenticated.” After the `AuthenticationManager` successfully validates it, it returns a new `Authentication` object that is marked “authenticated” and is now populated with the user’s details and granted authorities (roles/permissions).
  • SecurityContextHolder: This is the cornerstone of Spring Security’s identity management. It’s a thread-local storage utility that holds the `SecurityContext`, which in turn contains the `Authentication` object of the currently logged-in user. Our ultimate goal in manual authentication is to place a fully authenticated `Authentication` object into the `SecurityContextHolder`. Once it’s there, the rest of the framework recognizes the user as logged in for the duration of the request.
  • AuthenticationManager: This is the main strategy interface for authentication. It has a single method, `authenticate()`, which takes an `Authentication` object. Its job is to process the token and decide if the credentials are valid. If they are, it returns a fully populated, authenticated `Authentication` object. If not, it throws an `AuthenticationException`. You typically don’t implement this interface directly; instead, Spring Boot configures a `ProviderManager` implementation for you.
  • AuthenticationProvider: An `AuthenticationManager` delegates the actual authentication work to one or more `AuthenticationProvider`s. Each provider is responsible for a specific type of authentication. The most common one is `DaoAuthenticationProvider`, which works with a `UserDetailsService` to validate username/password credentials. You can have multiple providers to support different login methods simultaneously.
  • UserDetailsService: This is a simple data access object (DAO) interface that acts as a bridge to your user store (like a database, LDAP server, or in-memory collection). It has one method, `loadUserByUsername(String username)`, which is responsible for fetching the user’s record, including their hashed password and authorities. The `DaoAuthenticationProvider` uses this service to get the user data it needs to perform the password comparison.

The Step-by-Step Guide to Manual Authentication

Now, let’s put the theory into practice. We’ll build a simple REST endpoint at `/api/auth/login` that accepts a JSON object with a username and password. Upon successful validation, it will establish an authenticated session for the user, all done manually within our controller.

Step 1: Project Setup and Dependencies

First, ensure you have a standard Spring Boot project. The two essential starters for this task are `spring-boot-starter-web` (for the REST controller) and `spring-boot-starter-security`. Add the following to your `pom.xml`:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Step 2: Configure the AuthenticationManager

By default, Spring Boot 3.x and newer versions do not expose the `AuthenticationManager` as a bean that can be directly injected. To use it in our controller, we must explicitly expose it in our security configuration class. This is a critical step that unlocks the ability to perform manual authentication from any part of our application.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disabling for API-based authentication
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // Allow access to our login endpoint
                .anyRequest().authenticated() // Secure all other endpoints
            );
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // You still need a PasswordEncoder and UserDetailsService bean for the manager to work
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("password"))
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

In this configuration, we’ve permitted all requests to `/api/auth/**` so that unauthenticated users can access our login endpoint. We also provided a basic in-memory `UserDetailsService` and a `BCryptPasswordEncoder` so the `AuthenticationManager` has the necessary components to validate credentials.

Step 3: Create the Login DTO (Data Transfer Object)

Using a DTO is a clean way to map the incoming JSON request body to a Java object. We’ll use a simple Java `record` for this purpose.

public record LoginRequest(String username, String password) {}

Step 4: Build the Authentication Controller

This is where all the pieces come together. We’ll create a `@RestController` that injects our `AuthenticationManager` bean. The `login` method will orchestrate the entire manual authentication process.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;

    public AuthController(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
        try {
            // 1. Create an authentication token with the user's credentials
            Authentication authenticationRequest =
                UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());

            // 2. Authenticate the user by delegating to the AuthenticationManager
            Authentication authenticationResponse =
                this.authenticationManager.authenticate(authenticationRequest);

            // 3. If authentication is successful, set the authenticated user in the SecurityContext
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            context.setAuthentication(authenticationResponse);
            SecurityContextHolder.setContext(context);

            return ResponseEntity.ok("Login successful for user: " + authenticationResponse.getName());

        } catch (AuthenticationException e) {
            // 4. If authentication fails, return an unauthorized status
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
        }
    }
}

Dissecting the AuthController Logic

Let’s break down the four key steps within the `login` method:

  1. Create Authentication Token: We instantiate `UsernamePasswordAuthenticationToken` using the static `unauthenticated()` factory method. At this stage, it’s just a simple data container for the username and password provided by the user. The framework considers it untrusted.
  2. Delegate to AuthenticationManager: We pass this token to the `authenticate()` method. This is where the core security logic is triggered. The `AuthenticationManager` (which is a `ProviderManager` under the hood) finds a suitable `AuthenticationProvider` (our `DaoAuthenticationProvider`) to handle the token. The provider uses our `UserDetailsService` to fetch the user by username and the `PasswordEncoder` to securely compare the submitted password with the stored hash.
  3. Update the SecurityContextHolder: If the credentials are valid, the manager returns a new, fully populated `Authentication` object. This object is now marked as ‘authenticated’ and contains the user’s principal object and their `GrantedAuthority`s. We then take this trusted object and place it into the `SecurityContextHolder`. This is the most crucial step—it officially informs Spring Security that the user for the current thread of execution is now authenticated.
  4. Handle Failures: If the username is not found or the password does not match, the `authenticate()` method throws an `AuthenticationException` (e.g., a `BadCredentialsException`). Our `try-catch` block intercepts this, allowing us to return a clean HTTP 401 Unauthorized response to the client instead of a generic server error.

Important Considerations and Best Practices

While the example above is fully functional, a production-grade system requires a bit more nuance. Here are some key considerations:

  • Stateful vs. Stateless: Our example creates a stateful session. After a successful login, Spring Security’s `HttpSessionSecurityContextRepository` automatically saves the `SecurityContext` into the `HttpSession`. Subsequent requests from the same client (with the session cookie) will be automatically authenticated. For stateless REST APIs using technologies like JSON Web Tokens (JWT), you would not save the context to the session. Instead, after step 3, you would generate a JWT and return it in the response body. The client would then send this token in the `Authorization` header of subsequent requests.
  • CSRF Protection: We disabled Cross-Site Request Forgery (CSRF) protection for simplicity, which is common and safe for stateless, token-based APIs. However, if you are building a traditional stateful web application that uses sessions and cookies, you must keep CSRF enabled to prevent this critical vulnerability. Your frontend would need to include the CSRF token in its requests.
  • Asynchronous Security Context: The `SecurityContextHolder` uses a `ThreadLocal` strategy by default, meaning the authentication details are tied to the current request thread. If your authentication logic involves asynchronous operations (e.g., using `@Async` or Project Reactor/WebFlux), the security context will not propagate to the new threads automatically. You must configure a different mode or manually propagate the context to avoid losing the user’s identity in async tasks.
  • Robust Exception Handling: A simple `try-catch` block is fine for a demonstration, but a real application should use a centralized exception handling mechanism, such as a controller advice (`@ControllerAdvice`). This allows you to map different subtypes of `AuthenticationException` to more specific and informative HTTP error responses, improving the API experience for your clients.

Conclusion

Manually authenticating users in Spring Security unlocks a world of possibilities, giving you the ultimate flexibility to build custom login flows, secure modern RESTful APIs, and integrate complex, multi-step authentication requirements. By understanding the core components like the `AuthenticationManager`, `AuthenticationProvider`, and, most importantly, the `SecurityContextHolder`, you can step outside the comfortable bounds of `formLogin` and gain granular control over your application’s security. While Spring Security’s defaults are powerful and should be the first choice for standard scenarios, mastering this manual process is an essential skill that transforms you from a consumer of the framework into a true architect of your application’s security posture.

Scroll to Top