- Chapter
- Chapter 1 — Spring Security Fundamentals
- Prerequisites
- Topics 1.1–1.2
- You will be able to
- Explain how the security context is stored, persisted and cleared, access the principal and authorities correctly, map external claims to authorities and propagate the context to asynchronous work
- Covers
- SecurityContext · Authentication · Principal · Authorities
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally
- 4A. Deep Dive — Accessing and Propagating the Context
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
The SecurityContext holds the Authentication of the current execution. The Authentication carries the principal (who), the credentials (how they proved it, usually erased), and the authorities (what they are allowed). The SecurityContextHolder makes the context available to any code running in the request — by default through a ThreadLocal.

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String user = auth.getName(); // "alice" or JWT "sub"
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
Object principal = auth.getPrincipal(); // UserDetails, Jwt, OidcUser2Why It Exists
Authorisation decisions happen in many places — URL rules, method annotations, service logic, auditing — and they all need to know who the caller is. Passing the user through every method signature is impractical; the security context makes the identity available implicitly for the duration of the request, similar to how transactions and MDC work, while keeping authentication mechanisms decoupled from the code that consumes the identity.
3Core Concepts
3.1 SecurityContext

| Aspect | Detail |
|---|---|
| Interface | SecurityContext with getAuthentication() / setAuthentication() |
| Holder | SecurityContextHolder static access; SecurityContextHolderStrategy bean for injectable access |
| Creation | SecurityContextHolder.createEmptyContext() — never mutate the shared empty context |
| Clearing | FilterChainProxy clears at the end of each request |
3.2 Authentication
The Authentication in the context is the result of authentication: isAuthenticated() == true, credentials erased (eraseCredentialsAfterAuthentication), principal enriched (e.g. UserDetails loaded from the store). For unauthenticated requests, AnonymousAuthenticationFilter inserts an AnonymousAuthenticationToken with ROLE_ANONYMOUS.
3.3 Principal
| Mechanism | Principal type | Useful data |
|---|---|---|
Form/Basic with UserDetailsService | UserDetails (your adapter) | Username, flags, custom fields |
| JWT resource server | Jwt | Claims: sub, scope, custom claims |
| OAuth2/OIDC login | OidcUser / OAuth2User | ID token claims, user info |
| Opaque token | OAuth2AuthenticatedPrincipal | Introspection attributes |
3.4 Authorities
| Authority kind | Convention | Checked with |
|---|---|---|
| Role | ROLE_ADMIN | hasRole("ADMIN") |
| Permission | orders:approve | hasAuthority("orders:approve") |
| OAuth2 scope | SCOPE_orders.read (from scope/scp claim) | hasAuthority("SCOPE_orders.read") |
| MFA factor (Spring Security 7) | Factor authorities (e.g. password, OTT, WebAuthn factors) | Multi-factor authorization rules |
4How It Works Internally

1. SecurityContextHolderFilter supplies a deferred context loaded from the SecurityContextRepository (HTTP session, request attribute, or nothing for stateless chains).
2. Authentication filters set a new context on success and call securityContextRepository.saveContext(...) where persistence is configured.
3. Code reads the context through SecurityContextHolderStrategy (thread-local by default).
4. Spring MVC argument resolvers (@AuthenticationPrincipal, Authentication parameters) and SpEL in method security read the same context.
5. At the end of the request, the holder is cleared; the persisted copy (session) remains for the next request.
6. In WebFlux, ReactiveSecurityContextHolder stores the context in the Reactor Context instead of a thread-local.
4ADeep Dive — Accessing and Propagating the Context

// Controller: inject rather than use static access
@GetMapping("/orders")
List<OrderView> myOrders(@AuthenticationPrincipal Jwt jwt) {
return orders.forCustomer(jwt.getSubject());
}
// Custom meta-annotation for readability
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "claims['customer_id']")
public @interface CurrentCustomerId { }
@GetMapping("/profile")
Profile profile(@CurrentCustomerId String customerId) { ... }Propagation to other threads:
@Bean
ThreadPoolTaskExecutor reportExecutor() {
var ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(4);
// propagates trace and security context (with the context-propagation library)
ex.setTaskDecorator(new ContextPropagatingTaskDecorator());
return ex;
}
// or wrap explicitly:
Executor secured = new DelegatingSecurityContextExecutor(
Executors.newVirtualThreadPerTaskExecutor());| Scenario | Propagation approach |
|---|---|
@Async methods | DelegatingSecurityContextAsyncTaskExecutor or context-propagating task decorator |
CompletableFuture on custom executors | DelegatingSecurityContextExecutor |
| Virtual threads per task | Wrap the executor (thread-locals are not inherited automatically) |
| Messaging consumers | Re-establish identity from message metadata (service identity or propagated token) |
| Scheduled jobs | Run as a system/service principal explicitly |
5APIs / Features / Syntax
| API | Purpose |
|---|---|
SecurityContextHolder, SecurityContextHolderStrategy | Access context |
SecurityContextRepository (HttpSessionSecurityContextRepository, RequestAttributeSecurityContextRepository) | Persistence |
@AuthenticationPrincipal, @CurrentSecurityContext | Injection into controllers |
DelegatingSecurityContextExecutor, …AsyncTaskExecutor, …Runnable | Propagation |
ReactiveSecurityContextHolder | WebFlux |
GrantedAuthoritiesMapper, JwtAuthenticationConverter | Authority mapping |
@WithMockUser, @WithUserDetails, SecurityMockMvcRequestPostProcessors.jwt() | Testing |
6Practical Examples
6.1 Mapping JWT claims to authorities
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
var scopes = new JwtGrantedAuthoritiesConverter(); // scope → SCOPE_*
var roles = new JwtGrantedAuthoritiesConverter();
roles.setAuthoritiesClaimName("roles");
roles.setAuthorityPrefix("ROLE_");
var converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> Stream.concat(
scopes.convert(jwt).stream(), roles.convert(jwt).stream()).toList());
return converter;
}6.2 Using the principal in a service (without static access)
@Service
class AuditService {
private final SecurityContextHolderStrategy strategy =
SecurityContextHolder.getContextHolderStrategy();
void record(String action) {
String who = Optional.ofNullable(strategy.getContext().getAuthentication())
.map(Authentication::getName).orElse("system");
log.info("audit action={} user={}", action, who);
}
}6.3 Testing with a mock JWT
@Test
void listsOwnOrders() {
assertThat(mvc.get().uri("/orders")
.with(jwt().jwt(j -> j.subject("c-17"))
.authorities(new SimpleGrantedAuthority("SCOPE_orders.read"))))
.hasStatusOk();
}6.4 Running a job as a service principal
@Scheduled(cron = "0 0 * * * *")
void hourly() {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(UsernamePasswordAuthenticationToken.authenticated(
"svc-reporting", null, AuthorityUtils.createAuthorityList("ROLE_SYSTEM")));
SecurityContextHolder.setContext(ctx);
try { reports.generateHourly(); } finally { SecurityContextHolder.clearContext(); }
}6.5 Session vs stateless context persistence
| Chain type | Repository | Effect |
|---|---|---|
| Browser app with login | HttpSessionSecurityContextRepository | Identity survives across requests via session cookie |
| Stateless API (bearer tokens) | RequestAttributeSecurityContextRepository / none | Authenticate every request from the token |
| Mixed | Separate chains | Avoid accidental session creation for APIs |
7Common Problems & Mistakes
- Mutating the empty context instead of creating a new one.
- Static SecurityContextHolder calls scattered through business code.
- Lost identity in async code (no propagation).
- Inheritable thread-local mode leaking identities.
- Authority naming mismatches (
ROLE_prefix,SCOPE_prefix). - Expecting the context to be saved automatically in custom filters (explicit save since 6.0).
Several of these mistakes stay hidden until a specific path is exercised. Mutating the current context, by calling SecurityContextHolder.getContext().setAuthentication() instead of creating a new context, can expose an identity to other threads that share the same context instance, so always call createEmptyContext, set the authentication on the new instance and then call setContext. Static holder calls deep in business code make services untestable without a security setup and break silently on reactive paths. The missing-save problem typically appears after migrating a custom login filter to Spring Security 6 or 7: authentication succeeds, but the next request is anonymous because nothing called SecurityContextRepository.saveContext. Authority prefix mismatches are the most common cause of unexpected 403 responses: a JWT scope becomes SCOPE_orders.read, so a rule written as hasRole('orders.read') can never match. Enabling DEBUG logging for org.springframework.security prints the authentication and authorities each request carries, which usually resolves these problems within minutes.
8Performance & Production Considerations
| Concern | Guidance |
|---|---|
| Session size | Keep principal objects small when stored in sessions |
| Deferred loading | Context loaded lazily — avoid touching it unnecessarily in filters |
| Auditing | Record principal name and authentication type, never credentials |
| Clearing | Ensure custom executors clear contexts after tasks |
9Architecture & Design Considerations
- Identity via injection at the edges; domain services receive explicit user ids when needed.
- Authority mapping at the edge (converters) so the domain sees consistent authorities.
- Service principals for background work, never "no user".
- Context propagation as a platform concern (executors, messaging, tracing).
10Interview Questions & Answers
What is the SecurityContext?
The SecurityContext is the container for the Authentication of the current execution, meaning who the caller is and what they are allowed to do. It is a small interface with getAuthentication and setAuthentication. Code reaches it through SecurityContextHolder, which by default stores it in a ThreadLocal, so any component running in the request can read the identity without passing it through method signatures. Spring Security creates or loads a context for each request, authentication filters populate it, authorisation rules and method security read it, and FilterChainProxy clears it when the request ends. For browser sessions it is persisted between requests; for stateless APIs it is rebuilt from the token on every request.
How do you get the current user in a controller?
Let Spring MVC inject it rather than calling the static holder. Declaring an Authentication parameter gives the whole authentication, including name and authorities, while @AuthenticationPrincipal gives just the principal in its concrete type: a Jwt for a resource server, an OidcUser after OIDC login, or your UserDetails implementation for form login. The annotation also accepts a SpEL expression, so expression = "claims['customer_id']" extracts a single claim, and it can be wrapped in a custom meta-annotation such as @CurrentCustomerId for readability. @CurrentSecurityContext injects the whole context if needed. Injection keeps controllers easy to test, because MockMvc request post-processors such as jwt() can supply the principal directly.
What is the difference between a role and an authority?
In Spring Security an authority is any GrantedAuthority string attached to the Authentication, and a role is simply an authority that follows the ROLE_ prefix convention. hasAuthority('orders:approve') compares the string exactly, while hasRole('ADMIN') adds the prefix and checks for ROLE_ADMIN, so hasRole should be given the role name without the prefix. Conceptually, roles describe coarse groups of users, while authorities often represent fine-grained permissions or OAuth2 scopes, which Spring maps to SCOPE_ prefixed authorities. Mixing conventions is a frequent cause of 403 responses after switching from form login to JWTs, so decide on naming once, map claims to authorities at the edge, and use hasAuthority for permissions.
How is the security context stored by default in servlet applications?
Within a request it lives in a ThreadLocal, because SecurityContextHolder uses the MODE_THREADLOCAL strategy by default, and code should ideally reach it through the SecurityContextHolderStrategy bean. Between requests, persistence is the job of a SecurityContextRepository. SecurityContextHolderFilter loads a deferred context from that repository at the start of each request, and since Spring Security 6 authentication mechanisms save it explicitly on success. For form login the default is HttpSessionSecurityContextRepository, which stores it in the HTTP session, combined with RequestAttributeSecurityContextRepository for the current request. Stateless chains, such as JWT resource servers, persist nothing and authenticate every request from its token. In every case the ThreadLocal is cleared at the end.
Why is the security context missing in @Async methods?
Because the context is stored in a ThreadLocal on the request thread, and @Async runs the method on a different thread from the task executor, which starts with an empty context. Authorisation checks inside the async method then fail, and auditing records no user or the wrong one. The fix is to propagate the context at the moment the task is submitted. Wrap the executor in a DelegatingSecurityContextAsyncTaskExecutor, or set a ContextPropagatingTaskDecorator on the ThreadPoolTaskExecutor so that the security context, MDC and trace context travel together. The wrapper copies the context into the worker thread and clears it afterwards. Avoid inheritable-thread-local mode as a shortcut, because pooled threads keep stale identities.
How do you map custom JWT claims to authorities?
Configure a JwtAuthenticationConverter bean, which the resource server uses to turn a validated Jwt into a JwtAuthenticationToken. By default its JwtGrantedAuthoritiesConverter reads the scope or scp claim and prefixes each value with SCOPE_. For roles held in a custom claim, create a second JwtGrantedAuthoritiesConverter with setAuthoritiesClaimName("roles") and setAuthorityPrefix("ROLE_"), and combine both results in the converter's authorities function. For nested structures, such as Keycloak's realm_access.roles, write a small Converter<Jwt, Collection<GrantedAuthority>> that reads the map. setPrincipalClaimName lets you use a claim other than sub as the name. Unit test the converter directly with a Jwt built through Jwt.withTokenValue and assert the resulting authorities.
Why avoid inheritable thread-local mode?
MODE_INHERITABLETHREADLOCAL copies the parent's context into a child thread only at the moment the child thread is created, which does not match how modern applications use threads. Executor pools create threads once and reuse them, so a worker created during Alice's request keeps Alice's context and later runs Bob's tasks under her identity, or under a stale identity long after her session ended. That is both a security failure and an audit failure, and because it is intermittent it rarely shows up in tests. Explicit propagation with delegating executors or context-propagating task decorators copies the correct context for each task and clears it afterwards, which is predictable, testable and easy to review.
How should identity propagate through an event-driven system?
Treat identity as data that must be carried explicitly, because there is no ThreadLocal across a broker. At publication, the producer adds identity metadata to message headers: at minimum the authenticated subject, the tenant and a correlation id, and, where downstream authorisation needs it, a signed token, preferably a short-lived one obtained through token exchange rather than the user's original access token. Consumers validate that metadata and establish a SecurityContext for the duration of processing, then clear it. System-initiated work such as schedulers and retries runs under explicit service principals, never as no user. Every service audits the acting principal and the original requester, so actions can be attributed across the whole chain.