skip to content

How do you build a custom test annotation with @WithSecurityContext and a WithSecurityContextFactory?

level: seniorimportance: should knowfreq 34%

answer

  1. meta-annotation @WithSecurityContext(factory=...)
  2. implement WithSecurityContextFactory<A>
  3. createSecurityContext reads annotation attrs
  4. @Retention(RUNTIME) required
  5. factory can be a Spring bean; setupBefore timing

basics

~10 s

Create your own annotation meta-annotated with @WithSecurityContext(factory = MyFactory.class), then implement WithSecurityContextFactory<YourAnnotation> to build and return a SecurityContext. The listener puts that context into the SecurityContextHolder for the test.

solid answer

~30 s

When @WithMockUser/@WithUserDetails can't express your principal — e.g. a custom Authentication, OAuth2/JWT principal, or fields read from annotation parameters — you write a custom annotation. Define `@interface WithMockCustomUser` and meta-annotate it with `@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)`. The factory implements `WithSecurityContextFactory<WithMockCustomUser>`; its `createSecurityContext(WithMockCustomUser annotation)` reads the annotation's attributes, builds an Authentication (any type), sets it on `SecurityContextHolder.createEmptyContext()`, and returns it. The same `WithSecurityContextTestExecutionListener` invokes the factory and installs the result. The factory can be a Spring bean (constructor-injected), so it can call real collaborators like a UserDetailsService or JwtDecoder. You also control timing via the annotation's `setupBefore` (TestExecutionEvent).

code

java · 32 lines
java
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(
    factory = WithMockJwtUserFactory.class,
    setupBefore = TestExecutionEvent.TEST_METHOD)
public @interface WithMockJwtUser {
    String subject() default "alice";
    String[] scopes() default {"read"};
}

public class WithMockJwtUserFactory
        implements WithSecurityContextFactory<WithMockJwtUser> {

    @Override
    public SecurityContext createSecurityContext(WithMockJwtUser a) {
        Jwt jwt = Jwt.withTokenValue("test-token")
                .header("alg", "none")
                .subject(a.subject())
                .claim("scope", String.join(" ", a.scopes()))
                .build();
        var authorities = Arrays.stream(a.scopes())
                .map(s -> new SimpleGrantedAuthority("SCOPE_" + s))
                .collect(Collectors.toList());
        var auth = new JwtAuthenticationToken(jwt, authorities);
        SecurityContext ctx = SecurityContextHolder.createEmptyContext();
        ctx.setAuthentication(auth);
        return ctx;
    }
}

// usage:
// @Test @WithMockJwtUser(subject = "bob", scopes = {"read","write"})
// void ...

go deeper

for a junior

Awareness that custom test-user annotations can be built; not expected to implement.

for a middle

Know the two pieces: meta-annotation + factory implementing createSecurityContext.

for a senior

Implement it, inject collaborators via a bean factory, and choose setupBefore correctly.

for a principal

Design reusable domain test-auth annotations and reason about the listener/holder mechanism and timing across the suite.

## Why custom factories exist `@WithMockUser` builds a fixed `UsernamePasswordAuthenticationToken` with a framework `User`. Real apps sometimes need a **different `Authentication` type** (e.g. `JwtAuthenticationToken`, `OAuth2AuthenticationToken`, a bespoke token) or a **custom principal carrying domain data** driven by test-supplied attributes. The meta-annotation `@WithSecurityContext` is the extension point. ## The two pieces ### 1. Your annotation A runtime-retained annotation meta-annotated with `@WithSecurityContext`, naming the factory class: ```java @Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) public @interface WithMockCustomUser { String username() default "alice"; long id() default 42L; String[] authorities() default {"ROLE_USER"}; } ``` Attributes become the inputs your factory reads. ### 2. The factory Implements `org.springframework.security.test.context.support.WithSecurityContextFactory<A>` where `A` is your annotation: ```java public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> { @Override public SecurityContext createSecurityContext(WithMockCustomUser a) { SecurityContext ctx = SecurityContextHolder.createEmptyContext(); var principal = new AppUserPrincipal(a.id(), a.username()); var auth = new UsernamePasswordAuthenticationToken( principal, "n/a", AuthorityUtils.createAuthorityList(a.authorities())); ctx.setAuthentication(auth); return ctx; } } ``` ## How it's wired at runtime The **`WithSecurityContextTestExecutionListener`** (registered by spring-security-test) scans the test method/class for any annotation that is itself meta-annotated with `@WithSecurityContext`, instantiates the named factory, calls `createSecurityContext(...)`, and stores the returned context in `TestSecurityContextHolder` (hence `SecurityContextHolder`). This is the exact same machinery that powers `@WithMockUser` and `@WithUserDetails` — they are just built-in annotations using built-in factories. ## Making the factory a Spring bean If the factory needs collaborators (a `UserDetailsService`, `JwtDecoder`, repository), declare it as a **Spring bean** — the listener will resolve it from the `ApplicationContext` and use constructor injection. This lets a custom factory load real data or mint realistic tokens. ## Timing: setupBefore / TestExecutionEvent `@WithSecurityContext` (and your annotation, if you expose it) has a `setupBefore` of type `org.springframework.security.test.context.support.TestExecutionEvent`: - **`TEST_METHOD`** (default) — the context is created *after* `@BeforeEach`/`@Before` methods, right before the test body. Use this when your factory depends on data set up in `@BeforeEach`. - **`SETUP_TEST_METHOD`** — the context is created *before* `@BeforeEach` runs. Use this when your setup methods themselves need the authenticated context (e.g. a `@BeforeEach` that calls a secured service). ## Gotchas - The annotation **must** be `@Retention(RUNTIME)`, or the listener can't see it. - Return a context from `SecurityContextHolder.createEmptyContext()`; don't rely on mutating the current holder. - If the factory is *not* a bean but has a no-arg constructor, the listener instantiates it directly; only bean-style injection needs it registered in the context. - Prefer a custom factory over abusing `@WithUserDetails` when the principal can't come from a `UserDetailsService`. ## When to use Custom `Authentication` types (JWT/OAuth2/opaque), principals with test-driven domain fields, or reusable domain-specific test annotations shared across a codebase.

  • Your custom factory needs a JwtDecoder / repository. How do you inject it?
    Register the factory as a Spring bean with a constructor taking the collaborator. The WithSecurityContextTestExecutionListener resolves the factory from the ApplicationContext, so constructor injection works. A no-arg factory is instantiated directly without the context.
  • Your @BeforeEach calls a @PreAuthorize-protected service but the context isn't set yet. What do you change?
    Set setupBefore = TestExecutionEvent.SETUP_TEST_METHOD so the SecurityContext is established before @BeforeEach runs, instead of the default TEST_METHOD which sets it after setup methods.

saying these in an interview costs you the question

  • Forgetting @Retention(RUNTIME) on the custom annotation
  • Thinking you must configure the listener manually (spring-security-test auto-registers it)
  • Believing the factory can't use Spring collaborators
  • Confusing setupBefore semantics (TEST_METHOD after @BeforeEach vs SETUP_TEST_METHOD before it)

context