ArchitectMind
  1. Home
  2. Tutorials
  3. Book 4 · Spring & Spring Boot
  4. Volume VI
  5. Security Context
Book 4 · Volume VI — Spring Security, OAuth2 & Identity · Topic 1.3

Security Context

SecurityContext, Authentication, principal and authorities

8 min readStage 2 · Frameworks & design8 interview questions
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. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally
  5. 4A. Deep Dive — Accessing and Propagating the Context
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 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.

SecurityContext structure
Figure 1 — SecurityContext structure
JAVA
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, OidcUser

2Why 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

SecurityContextHolder strategies
Figure 2 — SecurityContextHolder strategies
AspectDetail
InterfaceSecurityContext with getAuthentication() / setAuthentication()
HolderSecurityContextHolder static access; SecurityContextHolderStrategy bean for injectable access
CreationSecurityContextHolder.createEmptyContext() — never mutate the shared empty context
ClearingFilterChainProxy clears at the end of each request
Table 1 — SecurityContext essentials

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

MechanismPrincipal typeUseful data
Form/Basic with UserDetailsServiceUserDetails (your adapter)Username, flags, custom fields
JWT resource serverJwtClaims: sub, scope, custom claims
OAuth2/OIDC loginOidcUser / OAuth2UserID token claims, user info
Opaque tokenOAuth2AuthenticatedPrincipalIntrospection attributes
Table 2 — Principal types

3.4 Authorities

Authority kindConventionChecked with
RoleROLE_ADMINhasRole("ADMIN")
Permissionorders:approvehasAuthority("orders:approve")
OAuth2 scopeSCOPE_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
Table 3 — Authority conventions

4How It Works Internally

Context lifecycle across requests
Figure 3 — Context lifecycle across requests

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

Accessing the current user
Figure 4 — Accessing the current user
JAVA
// 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:

JAVA
@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());
ScenarioPropagation approach
@Async methodsDelegatingSecurityContextAsyncTaskExecutor or context-propagating task decorator
CompletableFuture on custom executorsDelegatingSecurityContextExecutor
Virtual threads per taskWrap the executor (thread-locals are not inherited automatically)
Messaging consumersRe-establish identity from message metadata (service identity or propagated token)
Scheduled jobsRun as a system/service principal explicitly
Table 4 — Context propagation

5APIs / Features / Syntax

APIPurpose
SecurityContextHolder, SecurityContextHolderStrategyAccess context
SecurityContextRepository (HttpSessionSecurityContextRepository, RequestAttributeSecurityContextRepository)Persistence
@AuthenticationPrincipal, @CurrentSecurityContextInjection into controllers
DelegatingSecurityContextExecutor, …AsyncTaskExecutor, …RunnablePropagation
ReactiveSecurityContextHolderWebFlux
GrantedAuthoritiesMapper, JwtAuthenticationConverterAuthority mapping
@WithMockUser, @WithUserDetails, SecurityMockMvcRequestPostProcessors.jwt()Testing
Table 5 — Security context APIs

6Practical Examples

6.1 Mapping JWT claims to authorities

JAVA
@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)

JAVA
@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

JAVA
@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

JAVA
@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 typeRepositoryEffect
Browser app with loginHttpSessionSecurityContextRepositoryIdentity survives across requests via session cookie
Stateless API (bearer tokens)RequestAttributeSecurityContextRepository / noneAuthenticate every request from the token
MixedSeparate chainsAvoid accidental session creation for APIs
Table 6 — Choosing context persistence

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

ConcernGuidance
Session sizeKeep principal objects small when stored in sessions
Deferred loadingContext loaded lazily — avoid touching it unnecessarily in filters
AuditingRecord principal name and authentication type, never credentials
ClearingEnsure custom executors clear contexts after tasks
Table 7 — Production guidance

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

Q1BasicConcept

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.

Q2BasicCode

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.

Q3IntermediateTricky

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.

Q4IntermediateInternals

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.

Q5Senior EngineerScenario

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.

Q6Senior EngineerCode

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.

Q7Technical ArchitectTricky

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.

Q8Solution ArchitectDesign

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.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 4

Spring & Spring Boot

Volume I · Topic 1.1

Why Spring Exists

Spring Core, IoC & Dependency Injection

Volume I · Topic 1.2

Spring Framework Architecture

Spring Core, IoC & Dependency Injection

Volume I · Topic 1.3

Spring Modules

Spring Core, IoC & Dependency Injection

Volume II · Topic 1.1

Cross-Cutting Concerns

Spring AOP, Proxies, Events & Transactions

Volume II · Topic 1.2

Why Cross-Cutting Concerns Are Difficult

Spring AOP, Proxies, Events & Transactions

Volume II · Topic 1.3

Aspect-Oriented Programming

Spring AOP, Proxies, Events & Transactions

Volume III · Topic 1.1

Why Spring Boot Exists

Spring Boot, Configuration & Application Lifecycle

Volume III · Topic 1.2

Spring Boot Architecture

Spring Boot, Configuration & Application Lifecycle

Volume III · Topic 1.3

Spring Boot vs Spring Framework

Spring Boot, Configuration & Application Lifecycle

Volume IV · Topic 1.1

MVC

Spring MVC, REST & Web Applications

Volume IV · Topic 1.2

DispatcherServlet

Spring MVC, REST & Web Applications

Volume IV · Topic 1.3

MVC Request Lifecycle

Spring MVC, REST & Web Applications

Volume V · Topic 1.1

Repository Abstraction

Spring Data, JPA & Persistence

Volume V · Topic 1.2

Spring Data Architecture

Spring Data, JPA & Persistence

Volume V · Topic 1.3

Repository Types

Spring Data, JPA & Persistence

Volume VI · Topic 1.1

Security Architecture

Spring Security, OAuth2 & Identity

Volume VI · Topic 1.2

Security Filter Chain

Spring Security, OAuth2 & Identity

Volume VII · Topic 1.1

Enterprise Integration

Spring Integration, Testing & Cloud-Native Development

Volume VII · Topic 1.2

Integration Patterns

Spring Integration, Testing & Cloud-Native Development

Volume VII · Topic 1.3

Messaging Systems

Spring Integration, Testing & Cloud-Native Development