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? → checkAuthentication.getAuthorities() - What is the user’s ID? → cast
Authentication.getPrincipal()toUserDetails - 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:
| Strategy | Constant | Behaviour |
|---|---|---|
| Thread-local (default) | MODE_THREADLOCAL | One SecurityContext per thread. Cleared at end of request. |
| Inheritable thread-local | MODE_INHERITABLETHREADLOCAL | Child threads inherit the parent’s SecurityContext. |
| Global | MODE_GLOBAL | One 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 type | getPrincipal() type |
|---|---|
| Form login / JWT | UserDetails |
| HTTP Basic | UserDetails |
| OAuth2 login | OAuth2User or OidcUser |
| Anonymous | "anonymousUser" (String) |
| Custom | Whatever 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)
}
}
In a Controller (Recommended)
@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]
SecurityContextHolderholds theSecurityContextusingThreadLocalby default.Authenticationin the context has three key properties:principal(who),authorities(what they can do),credentials(how they proved it — usually cleared after auth).- Use
@AuthenticationPrincipalin controllers; useSecurityContextHolderonly in services where injection is not possible. - For async code: wrap executors with
DelegatingSecurityContextAsyncTaskExecutoror useDelegatingSecurityContextRunnable. - For stateless APIs: use
SessionCreationPolicy.STATELESSso the context is never stored in a session.
Next: Article 4 covers AuthenticationManager, AuthenticationProvider, and UserDetailsService — the internal delegation chain that actually verifies credentials.