SecurityContext and Authentication Object

The SecurityContext Is the Source of Truth

Every security decision in Spring Security ultimately comes down to one question: “What Authentication object is stored in the SecurityContext?”

  • Does the user have ROLE_ADMIN? → check Authentication.getAuthorities()
  • What is the user’s ID? → cast Authentication.getPrincipal() to UserDetails
  • Is the user logged in at all? → check Authentication.isAuthenticated()

Understanding the SecurityContext and Authentication is not optional — it underlies everything.


The Object Hierarchy

classDiagram
    class SecurityContextHolder {
        -strategy: SecurityContextHolderStrategy
        +getContext() SecurityContext
        +setContext(SecurityContext context)
        +clearContext()
        +getContextHolderStrategy() SecurityContextHolderStrategy
    }

    class SecurityContext {
        <>
        +getAuthentication() Authentication
        +setAuthentication(Authentication authentication)
    }

    class Authentication {
        <>
        +getPrincipal() Object
        +getCredentials() Object
        +getAuthorities() Collection~GrantedAuthority~
        +getDetails() Object
        +isAuthenticated() boolean
        +getName() String
    }

    class UsernamePasswordAuthenticationToken {
        +UsernamePasswordAuthenticationToken(principal, credentials)
        +UsernamePasswordAuthenticationToken(principal, credentials, authorities)
    }

    class GrantedAuthority {
        <>
        +getAuthority() String
    }

    class SimpleGrantedAuthority {
        -role: String
        +getAuthority() String
    }

    SecurityContextHolder --> SecurityContext
    SecurityContext --> Authentication
    Authentication <|.. UsernamePasswordAuthenticationToken
    Authentication --> GrantedAuthority
    GrantedAuthority <|.. SimpleGrantedAuthority

SecurityContextHolder Storage Strategies

SecurityContextHolder stores the SecurityContext. By default it uses a ThreadLocal — each thread gets its own isolated context. Three strategies are available:

StrategyConstantBehaviour
Thread-local (default)MODE_THREADLOCALOne SecurityContext per thread. Cleared at end of request.
Inheritable thread-localMODE_INHERITABLETHREADLOCALChild threads inherit the parent’s SecurityContext.
GlobalMODE_GLOBALOne SecurityContext shared by the entire JVM (never use in web apps).
// Change strategy (rare — usually leave as default)
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

Why MODE_INHERITABLETHREADLOCAL Matters

If you spawn threads manually (e.g., new Thread(() -> doWork()).start()), the child thread does not inherit the ThreadLocal context by default. This matters when:

  • Sending emails in a background thread after processing a request
  • Logging the authenticated username in an async task
// Thread A (request thread) — has SecurityContext with "alice"
new Thread(() -> {
    // Thread B — has NULL SecurityContext with MODE_THREADLOCAL
    Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // null!
}).start();

With MODE_INHERITABLETHREADLOCAL:

new Thread(() -> {
    // Thread B — inherits SecurityContext from Thread A
    Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // alice ✓
}).start();

For Spring’s own async mechanisms (@Async, CompletableFuture), see the section on async below.


The Authentication Interface

Authentication is the central object. It has five properties:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // roles and permissions
    Object getCredentials();  // password (cleared after authentication, often null)
    Object getDetails();      // extra details: IP address, session ID
    Object getPrincipal();    // the authenticated entity (UserDetails or username string)
    boolean isAuthenticated();
}

Before vs After Authentication

The same UsernamePasswordAuthenticationToken class is used for both stages:

// BEFORE authentication — isAuthenticated() = false
// Created by UsernamePasswordAuthenticationFilter from the login request
UsernamePasswordAuthenticationToken request =
    new UsernamePasswordAuthenticationToken("alice", "password");

// AFTER authentication — isAuthenticated() = true
// Created by AuthenticationProvider after credential verification
UsernamePasswordAuthenticationToken authenticated =
    new UsernamePasswordAuthenticationToken(
        userDetails,  // principal (UserDetails object)
        null,         // credentials (cleared for security)
        userDetails.getAuthorities()  // populated authorities
    );

The three-argument constructor sets isAuthenticated = true. The two-argument constructor sets it false.

Principal: What It Actually Is

getPrincipal() returns Object — the actual type depends on how authentication was performed:

Authentication typegetPrincipal() type
Form login / JWTUserDetails
HTTP BasicUserDetails
OAuth2 loginOAuth2User or OidcUser
Anonymous"anonymousUser" (String)
CustomWhatever your AuthenticationProvider returns
// Safely extract UserDetails from principal
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof UserDetails userDetails) {
    String username = userDetails.getUsername();
    boolean isAdmin = userDetails.getAuthorities().stream()
        .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}

GrantedAuthority: Roles and Permissions

GrantedAuthority represents a permission or role granted to the authenticated user. Implementations are simple:

// Role
new SimpleGrantedAuthority("ROLE_USER");
new SimpleGrantedAuthority("ROLE_ADMIN");

// Permission
new SimpleGrantedAuthority("product:read");
new SimpleGrantedAuthority("order:write");

Spring Security’s hasRole("ADMIN") automatically prepends ROLE_hasRole("ADMIN") checks for ROLE_ADMIN. hasAuthority("product:read") checks the exact string.


Accessing the Current User

In a Service or Component

@Service
public class OrderService {

    public Order createOrder(OrderRequest request) {
        // Method 1: static access
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = auth.getName();

        // Method 2: inject via @AuthenticationPrincipal in controller and pass down
        // (preferred for testability)
    }
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/my")
    public List<Order> myOrders(@AuthenticationPrincipal UserDetails currentUser) {
        // Spring injects the current UserDetails directly — no SecurityContextHolder call
        return orderService.findByUsername(currentUser.getUsername());
    }

    @GetMapping("/me")
    public UserProfileDto profile(Authentication authentication) {
        // Spring injects the full Authentication object
        UserDetails user = (UserDetails) authentication.getPrincipal();
        return UserProfileDto.from(user);
    }
}

@AuthenticationPrincipal is the cleanest approach — it makes the dependency explicit and controllers are testable without a live security context.

Custom @CurrentUser Annotation

Create a meta-annotation for cleaner code:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentUser {}
@GetMapping("/profile")
public UserProfileDto getProfile(@CurrentUser AppUserDetails user) {
    return UserProfileDto.from(user);
}

How the Context Is Persisted Between Requests

For stateful web apps (form login + sessions), the SecurityContext is saved to the HTTP session after each authenticated request and restored at the start of the next one.

sequenceDiagram
    participant Client
    participant SCFilter as SecurityContextHolderFilter
    participant Repo as HttpSessionSecurityContextRepository
    participant Chain as Filter Chain + Controller

    Note over Client,Chain: Request 1: POST /login (authentication)
    Client->>SCFilter: POST /login
    SCFilter->>Repo: load(request) → empty context
    SCFilter->>Chain: doFilter()
    Chain->>Chain: UsernamePasswordAuthFilter authenticates alice
    Chain->>Chain: SecurityContextHolder.getContext().setAuthentication(alice)
    SCFilter->>Repo: save(context, response) → store in HTTP session
    SCFilter-->>Client: 200 OK (JSESSIONID cookie)

    Note over Client,Chain: Request 2: GET /api/orders (authenticated)
    Client->>SCFilter: GET /api/orders (JSESSIONID cookie)
    SCFilter->>Repo: load(request) → SecurityContext with alice
    SCFilter->>Chain: doFilter() — alice is authenticated
    Chain-->>Client: 200 OK (orders list)
    SCFilter->>Repo: save(context unchanged) — noop

For stateless APIs (JWT), the SecurityContextRepository is replaced with RequestAttributeSecurityContextRepository — the context exists only for the current request and is never stored in the session.

// Stateless configuration — context never stored in session
http
    .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .securityContext(ctx ->
        ctx.securityContextRepository(new RequestAttributeSecurityContextRepository()));

SecurityContext in Async Code

@Async Methods

Spring’s @Async does NOT propagate the SecurityContext by default. Use DelegatingSecurityContextAsyncTaskExecutor:

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.initialize();
        // Wrap with security context delegation
        return new DelegatingSecurityContextAsyncTaskExecutor(executor);
    }
}

Now @Async methods automatically receive the caller’s SecurityContext.

CompletableFuture

// Capture context before async work
SecurityContext context = SecurityContextHolder.getContext();

CompletableFuture.runAsync(() -> {
    // Set captured context on the async thread
    SecurityContextHolder.setContext(context);
    try {
        doWork(); // can call auth.getName() here
    } finally {
        SecurityContextHolder.clearContext(); // always clean up
    }
});

Or use Spring’s DelegatingSecurityContextRunnable:

Runnable securedTask = new DelegatingSecurityContextRunnable(() -> doWork());
CompletableFuture.runAsync(securedTask);

Setting Authentication Programmatically

Sometimes you need to set an authentication manually — for example, after JWT validation in a custom filter, or during testing:

// In a custom authentication filter
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

UsernamePasswordAuthenticationToken authentication =
    new UsernamePasswordAuthenticationToken(
        userDetails,
        null,
        userDetails.getAuthorities()
    );
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
// In a test — programmatically set authentication
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(
    new UsernamePasswordAuthenticationToken("alice", null,
        List.of(new SimpleGrantedAuthority("ROLE_USER")))
);
SecurityContextHolder.setContext(context);

Clearing the SecurityContext

The SecurityContextHolderFilter clears the context after each request. But in custom code — especially in tests or background threads — you must clear it manually:

finally {
    SecurityContextHolder.clearContext();
}

Failure to clear leads to context leaking between requests (if threads are reused in a thread pool) — a serious security bug.


Complete Example: Custom UserDetails

// Custom UserDetails with extra fields (userId, email)
public class AppUserDetails implements UserDetails {

    private final Long id;
    private final String username;
    private final String email;
    private final String password;
    private final List<GrantedAuthority> authorities;

    public AppUserDetails(User user) {
        this.id = user.getId();
        this.username = user.getUsername();
        this.email = user.getEmail();
        this.password = user.getPassword();
        this.authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
    }

    @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
    @Override public String getPassword() { return password; }
    @Override public String getUsername() { return username; }
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }

    // Extra fields
    public Long getId() { return id; }
    public String getEmail() { return email; }
}
// Custom annotation for cleaner injection
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentUser {}

// Controller usage
@GetMapping("/profile")
public UserProfileDto profile(@CurrentUser AppUserDetails user) {
    return new UserProfileDto(user.getId(), user.getUsername(), user.getEmail());
}

Summary

flowchart LR
    Request[HTTP Request] --> SCFilter[SecurityContextHolderFilter]
    SCFilter -->|load from| Repo[(Session / Request Attr)]
    Repo --> Context[SecurityContext]
    Context --> Auth[Authentication\nprincipal, authorities, credentials]
    Auth --> Filters[Authentication Filters]
    Filters -->|set authenticated| Context
    Context --> AuthFilter[AuthorizationFilter\nchecks authorities]
    AuthFilter --> Controller[Controller\n@AuthenticationPrincipal]
    SCFilter -->|save to| Repo
    SCFilter -->|clear| Holder[SecurityContextHolder]
  • SecurityContextHolder holds the SecurityContext using ThreadLocal by default.
  • Authentication in the context has three key properties: principal (who), authorities (what they can do), credentials (how they proved it — usually cleared after auth).
  • Use @AuthenticationPrincipal in controllers; use SecurityContextHolder only in services where injection is not possible.
  • For async code: wrap executors with DelegatingSecurityContextAsyncTaskExecutor or use DelegatingSecurityContextRunnable.
  • For stateless APIs: use SessionCreationPolicy.STATELESS so the context is never stored in a session.

Next: Article 4 covers AuthenticationManager, AuthenticationProvider, and UserDetailsService — the internal delegation chain that actually verifies credentials.