Home Java Spring Security Manual Authentication: A Senior Dev's Guide
Intermediate 4 min · July 14, 2026
How to Manually Authenticate a User in Spring Security

Spring Security Manual Authentication: A Senior Dev's Guide

Learn how to implement manual authentication in Spring Security 6.2 with real production insights, debugging tips, and code examples.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and Spring Security
  • Familiarity with dependency injection and configuration
  • Understanding of HTTP and REST APIs
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use AuthenticationManager and UsernamePasswordAuthenticationToken for manual login.
  • Always store SecurityContext in a SecurityContextHolder strategy.
  • Validate credentials against your user service before creating the token.
  • Handle exceptions like BadCredentialsException and LockedException gracefully.
  • Never expose authentication logic in controllers; use a dedicated service.
✦ Definition~90s read
What is How to Manually Authenticate a User in Spring Security?

Manual authentication in Spring Security is the process where you explicitly call AuthenticationManager.authenticate() to validate credentials and set the security context, bypassing the auto-configuration filters.

Think of Spring Security as a bouncer at a club.
Plain-English First

Think of Spring Security as a bouncer at a club. Automatic authentication is like having a guest list that the bouncer checks automatically. Manual authentication is when you walk up to the bouncer, show your ID yourself, and he decides to let you in after verifying it. You control the process step by step.

You're building a SaaS billing system. Users log in via a custom form, but you need to validate credentials against a legacy CRM and then issue a JWT. Spring Security's auto-config doesn't cut it—you need manual control. This is where manual authentication shines.

Most tutorials show you how to use Spring Security's auto-configuration with a login form. That's fine for demos. In production, you'll need to authenticate users manually—especially when integrating with external systems, supporting multiple authentication sources, or building a REST API.

In this guide, I'll show you how to implement manual authentication in Spring Security 6.2. We'll cover the core components: AuthenticationManager, AuthenticationProvider, and SecurityContextHolder. You'll see real code, production pitfalls, and debugging techniques I've learned from years of building secure systems.

By the end, you'll be able to implement custom login flows that are secure, testable, and maintainable. No more fighting with auto-config.

Understanding the Authentication Architecture

Before writing code, you need to understand Spring Security's authentication architecture. It's a chain of components:

  • AuthenticationFilter: Intercepts the request and extracts credentials.
  • AuthenticationManager: The orchestrator that delegates to providers.
  • AuthenticationProvider: Contains the actual logic to validate credentials.
  • SecurityContextHolder: Stores the authenticated principal for the request.

In manual authentication, you bypass the filter chain and directly interact with AuthenticationManager. This gives you full control.

Here's the flow: 1. You create an Authentication object (usually UsernamePasswordAuthenticationToken). 2. You call AuthenticationManager.authenticate(token). 3. The manager delegates to a matching AuthenticationProvider. 4. If successful, you get back a fully populated Authentication object. 5. You set it in SecurityContextHolder.

This pattern is used in REST APIs where you authenticate once and return a token (JWT) that the client sends on subsequent requests.

I've seen teams overcomplicate this by writing their own authentication logic from scratch. Don't. Spring Security's AuthenticationManager is well-tested and handles edge cases you'll miss.

ManualAuthenticationService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
public class ManualAuthenticationService {

    private final AuthenticationManager authenticationManager;

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

    public Authentication authenticate(String username, String password) {
        UsernamePasswordAuthenticationToken token = 
            new UsernamePasswordAuthenticationToken(username, password);
        Authentication authentication = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        return authentication;
    }
}
💡Always return the Authentication object
📊 Production Insight
In production, never call authenticationManager.authenticate() from a controller. Wrap it in a service that can handle exceptions and logging consistently.
🎯 Key Takeaway
Manual authentication uses AuthenticationManager directly, giving you complete control over the login flow.

Configuring AuthenticationManager Bean

To use manual authentication, you need to provide an AuthenticationManager bean. In Spring Security 6.2, the common approach is to define an AuthenticationConfiguration and expose the manager.

```java @Configuration @EnableWebSecurity public class SecurityConfig {

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(csrf -> csrf.disable()); return http.build(); }

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

I prefer this over using AuthenticationManagerBuilder because it's cleaner and leverages the auto-configuration.

One gotcha: if you have multiple AuthenticationProvider beans, they are automatically picked up by the AuthenticationManager. But if you need a specific order, use @Order or manually build the manager with ProviderManager.

What the docs don't tell you: If you declare your own AuthenticationManager bean, Spring Boot's auto-configuration will back off. You'll lose the default providers. Always ensure you're not accidentally overriding the bean.

SecurityConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable());
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}
⚠ Don't forget to disable CSRF for stateless APIs
📊 Production Insight
I once saw a team spend hours debugging why their custom AuthenticationProvider wasn't invoked. The culprit: they had declared an AuthenticationManager bean that used AuthenticationManagerBuilder without adding the custom provider. Spring Boot's auto-configuration backed off, and the provider was never registered.
🎯 Key Takeaway
Expose AuthenticationManager as a bean using AuthenticationConfiguration to keep your configuration clean.

Implementing a Custom AuthenticationProvider

The AuthenticationProvider is where you put your custom authentication logic. For example, validating credentials against a legacy CRM or an LDAP server.

Here's a custom provider that checks credentials against a database:

```java @Component public class DatabaseAuthenticationProvider implements AuthenticationProvider {

private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder;

public DatabaseAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService = userDetailsService; this.passwordEncoder = passwordEncoder; }

@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString();

UserDetails user = userDetailsService.loadUserByUsername(username);

if (!passwordEncoder.matches(password, user.getPassword())) { throw new BadCredentialsException("Bad credentials"); }

if (!user.isEnabled()) { throw new DisabledException("User disabled"); }

return new UsernamePasswordAuthenticationToken( user, password, user.getAuthorities()); }

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

Notice the supports() method. It tells Spring Security which Authentication type this provider handles. If you forget it, your provider won't be called.

Production insight: Always check for isEnabled(), isAccountNonLocked(), etc. I've debugged cases where a locked account was still able to authenticate because the provider only checked the password.

DatabaseAuthenticationProvider.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DatabaseAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public DatabaseAuthenticationProvider(UserDetailsService userDetailsService, 
                                           PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails user = userDetailsService.loadUserByUsername(username);

        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("Bad credentials");
        }

        if (!user.isEnabled()) {
            throw new DisabledException("User disabled");
        }

        return new UsernamePasswordAuthenticationToken(
            user, password, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication);
    }
}
💡Use meaningful exception types
📊 Production Insight
In production, always log authentication failures with a correlation ID, but never log the password or the full stack trace. Use a structured logging format like JSON for easy parsing in log aggregators.
🎯 Key Takeaway
Custom AuthenticationProvider gives you full control over credential validation, including account status checks.

Handling Authentication Exceptions

Authentication exceptions like BadCredentialsException and LockedException need to be handled properly. In a REST API, you typically return a 401 with a JSON error body.

You can customize the response using a custom AuthenticationEntryPoint:

```java @Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); String errorMessage = "Authentication failed: " + authException.getMessage(); response.getWriter().write("{\"error\":\"" + errorMessage + "\"}"); } } ```

``java .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(customAuthenticationEntryPoint) ) ``

What the official docs won't tell you: The AuthenticationEntryPoint is only invoked for authentication exceptions. For access denied (authorization) exceptions, you need a separate AccessDeniedHandler. Many developers confuse the two.

Also, if you're using JWT, you might want to return different error messages for expired tokens vs. invalid tokens. I've seen teams expose too much information in error messages, which can aid attackers. Keep error messages generic, but log the details server-side.

CustomAuthenticationEntryPoint.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        String errorMessage = "Authentication failed: " + authException.getMessage();
        response.getWriter().write("{\"error\":\"" + errorMessage + "\"}");
    }
}
⚠ Don't expose sensitive details in error messages
📊 Production Insight
In production, I recommend using a global exception handler with @ControllerAdvice for REST APIs. This centralizes error handling and allows you to return consistent JSON structures for both authentication and authorization errors.
🎯 Key Takeaway
Use a custom AuthenticationEntryPoint to control the HTTP response for authentication failures.

What the Official Docs Won't Tell You

I've been using Spring Security for over a decade. Here are the hard-learned truths:

1. SecurityContextHolder Strategy is Global, Not Per-Request

Many developers assume the strategy (e.g., MODE_INHERITABLETHREADLOCAL) can be set per request. It can't. It's a JVM-wide setting. If you set it in one filter, it affects the entire application. The only safe way is to set it once at application startup.

2. AuthenticationManager Bean Override Pitfalls

If you define your own AuthenticationManager bean, Spring Boot's auto-configuration will not create the default one. This means you lose all default providers (like the one for in-memory users). Always explicitly add your providers if you override.

3. Exception Handling in Filters

The AuthenticationEntryPoint only handles exceptions thrown during authentication. If your filter chain is misconfigured, you might get exceptions that bypass the entry point. Always test with a misconfigured filter to see the actual response.

4. Stateless vs Stateful Session Management

If you're using manual authentication for a REST API, set session creation policy to STATELESS. Otherwise, Spring Security will create a session even if you don't need one. This can lead to unexpected behavior with CSRF and session fixation.

5. Thread Safety of SecurityContextHolder

By default, SecurityContextHolder uses ThreadLocal. If you spawn new threads, the security context is not propagated. Use MODE_INHERITABLETHREADLOCAL or wrap tasks with DelegatingSecurityContextRunnable. I've seen production outages because of this.

6. Testing Manual Authentication

Testing manual authentication requires mocking AuthenticationManager and verifying SecurityContextHolder. Use SecurityContextHolderTestExecutionListener in Spring Boot tests to clear the context after each test.

These are the gotchas that will save you hours of debugging.

SecurityContextConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class SecurityContextConfig {

    @PostConstruct
    public void init() {
        SecurityContextHolder.setStrategyName(
            SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }
}
⚠ Set SecurityContextHolder strategy early
📊 Production Insight
In one production incident, a team spent two days debugging random 401 errors. The root cause: they had set SecurityContextHolder.setStrategyName inside a controller method, which only applied to the current thread. Async tasks ran with the default strategy and lost the context.
🎯 Key Takeaway
The official docs gloss over thread safety and bean override issues. Always test your authentication under realistic conditions.

Integrating with JWT Token Generation

In many projects, manual authentication is used to generate a JWT token that the client sends on subsequent requests. Here's how it fits together:

  1. User sends username/password to /api/auth/login.
  2. Your service calls AuthenticationManager.authenticate().
  3. If successful, generate a JWT and return it.
  4. On subsequent requests, a filter extracts the JWT and sets the security context.

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

private final ManualAuthenticationService authService; private final JwtTokenProvider tokenProvider;

public AuthController(ManualAuthenticationService authService, JwtTokenProvider tokenProvider) { this.authService = authService; this.tokenProvider = tokenProvider; }

@PostMapping("/login") public ResponseEntity<JwtResponse> login(@RequestBody LoginRequest request) { Authentication authentication = authService.authenticate( request.getUsername(), request.getPassword()); String token = tokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtResponse(token)); } } ```

Production insight: Never store the Authentication object in the session for a JWT-based API. The client should send the token with each request. If you store the context in the session, you defeat the purpose of stateless authentication.

Also, ensure your JWT filter sets SecurityContextHolder with the token's authentication. Otherwise, the request will be unauthenticated.

AuthController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

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

    private final ManualAuthenticationService authService;
    private final JwtTokenProvider tokenProvider;

    public AuthController(ManualAuthenticationService authService, 
                          JwtTokenProvider tokenProvider) {
        this.authService = authService;
        this.tokenProvider = tokenProvider;
    }

    @PostMapping("/login")
    public ResponseEntity<JwtResponse> login(@RequestBody LoginRequest request) {
        Authentication authentication = authService.authenticate(
            request.getUsername(), request.getPassword());
        String token = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new JwtResponse(token));
    }
}
💡Clear SecurityContext after generating token
📊 Production Insight
I've seen teams forget to clear the SecurityContext after login, leading to authentication persisting across requests even without a valid token. This is a security vulnerability.
🎯 Key Takeaway
Manual authentication is the foundation for JWT-based authentication. Always clear the context after token generation in stateless APIs.
● Production incidentPOST-MORTEMseverity: high

The Midnight Auth Loop: How a Missing `SecurityContext` Caused a Production Outage

Symptom
Users were authenticated successfully, but subsequent API calls returned 401 Unauthorized. The system worked fine under low load but failed under concurrency.
Assumption
The developer assumed that setting the SecurityContext in the request thread would automatically propagate to child threads and async operations.
Root cause
SecurityContextHolder was configured with MODE_INHERITABLETHREADLOCAL in some places and MODE_THREADLOCAL in others. Async tasks lost the security context because they ran on different threads.
Fix
Set a global SecurityContextHolder strategy to MODE_INHERITABLETHREADLOCAL and ensure all async executors use DelegatingSecurityContextAsyncTaskExecutor.
Key lesson
  • Always configure SecurityContextHolder strategy globally, not per-request.
  • Use DelegatingSecurityContextRunnable or DelegatingSecurityContextExecutor for async tasks.
  • Test authentication under concurrent load to catch thread propagation issues.
  • Log security context presence in filters to debug propagation.
  • Document the threading model for your authentication flow.
Production debug guideSymptom to Action3 entries
Symptom · 01
User gets 401 after successful login
Fix
Check if SecurityContext is stored in the session. Verify SecurityContextPersistenceFilter is configured. Look for missing SecurityContextHolder.setContext() after authentication.
Symptom · 02
Authentication works locally but fails in production
Fix
Compare environment configurations: session management, security filter chain order, and SecurityContextHolder strategy. Check if behind a proxy that strips authentication headers.
Symptom · 03
BadCredentialsException thrown for valid credentials
Fix
Verify password encoder is consistent. Check if UserDetailsService returns correct user. Look at AuthenticationProvider.supports() method.
★ Quick Debug Cheat SheetCommon manual auth issues and immediate fixes
401 after login
Immediate action
Add a breakpoint in your custom `AuthenticationProvider.authenticate()`
Commands
curl -v -X POST /login -d 'username=admin&password=admin'
check response headers for Set-Cookie
Fix now
Ensure you call SecurityContextHolder.getContext().setAuthentication(authentication)
Async tasks lose authentication+
Immediate action
Check `SecurityContextHolder` strategy
Commands
System.out.println(SecurityContextHolder.getContextHolderStrategy());
verify async executor wraps tasks
Fix now
Set SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL)
Password mismatch+
Immediate action
Log the encoded password from DB and the raw password attempts
Commands
System.out.println(passwordEncoder.encode("rawPassword"));
compare with stored hash
Fix now
Ensure PasswordEncoder matches the one used during registration
FeatureAuto-ConfigurationManual Authentication
ControlLimited to defaultsFull control
ComplexitySimple setupMore code but flexible
Custom ProvidersAuto-registeredMust be explicitly added
Thread SafetyDefault ThreadLocalCan be configured globally
Best ForSimple form loginREST APIs, JWT, legacy systems
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
ManualAuthenticationService.java@ServiceUnderstanding the Authentication Architecture
SecurityConfig.java@ConfigurationConfiguring AuthenticationManager Bean
DatabaseAuthenticationProvider.java@ComponentImplementing a Custom AuthenticationProvider
CustomAuthenticationEntryPoint.java@ComponentHandling Authentication Exceptions
SecurityContextConfig.java@ComponentWhat the Official Docs Won't Tell You
AuthController.java@RestControllerIntegrating with JWT Token Generation

Key takeaways

1
Manual authentication gives you full control over the login flow in Spring Security.
2
Always set SecurityContextHolder strategy globally and clear context in stateless APIs.
3
Custom AuthenticationProvider allows integration with any user store.
4
Handle authentication exceptions with a custom AuthenticationEntryPoint for consistent error responses.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how to implement manual authentication in Spring Security.
Q02SENIOR
How would you handle authentication exceptions in a REST API?
Q03SENIOR
What are the thread-safety concerns with `SecurityContextHolder`?
Q01 of 03SENIOR

Explain how to implement manual authentication in Spring Security.

ANSWER
Inject AuthenticationManager and call authenticate() with a UsernamePasswordAuthenticationToken. Then set the result in SecurityContextHolder. Provide a custom AuthenticationProvider if needed.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between `AuthenticationManager` and `AuthenticationProvider`?
02
How do I test manual authentication in Spring Boot?
03
Why is my custom `AuthenticationProvider` not being called?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Security. Mark it forged?

4 min read · try the examples if you haven't

Previous
Custom AuthenticationProvider in Spring Security: Dao and Custom Providers
13 / 31 · Spring Security
Next
Spring Security Custom AuthenticationFailureHandler Implementation