Summary

The SecurityMockMvcResultMatchers do not work when SessionCreationPolicy.STATELESS is used.

All examples from https://stackoverflow.com/questions/37550039/test-spring-with-mockmvc

Actual Behavior

The tests fails with the following exception java.lang.AssertionError: Authentication should not be null.

java.lang.AssertionError: Authentication should not be null

at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:39)
at org.springframework.test.util.AssertionErrors.assertTrue(AssertionErrors.java:72)
at org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers$AuthenticatedMatcher.match(SecurityMockMvcResultMatchers.java:98)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
at br.com.bla.studying.integration.LoginTests.login(LoginTests.java:70)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Expected Behavior

It should be possible to use this matcher even for stateless rest services.

@Test
public void login() throws Exception {
    final LoginRequest loginRequest = new LoginRequest();
    loginRequest.setEmail(EMAIL);
    loginRequest.setPassword(PASSWORD);

    mockMvc.perform(post(ENDPOINT)
            .contentType(TestUtil.APPLICATION_JSON_UTF8)
            .content(TestUtil.convertObjectToJsonBytes(loginRequest))
    ).andExpect(authenticated());
}

Configuration

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

@Bean
public AuthenticationFilter authenticationFilterBean() throws Exception {
    final AuthenticationFilter authenticationFilter = new AuthenticationFilter();
    authenticationFilter.setAuthenticationManager(this.authenticationManagerBean());
    return authenticationFilter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
        .authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers(HttpMethod.POST, "/users").permitAll()
            .anyRequest().authenticated();

    http.addFilterBefore(authenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);
}

}

Version

Verified with * spring-security 4.2.3.RELEASE * spring-boot 1.5.4.RELEASE * spring 4.3.9.RELEASE

Sample

See https://stackoverflow.com/questions/37550039/test-spring-with-mockmvc for the full example

Comment From: rwinch

Thanks for the report!

I see what you need to do, but this is behaving as I would expect. The assertion is verifying if a user is currently authenticated (not if someone did authenticate). Because it is stateless, the user is no longer authenticated after the request completes.

I need to give the semantics of this a bit of thought, because I understand that there is a use case that is not being covered right now.

Comment From: afayes

We are using stateless authentication with JWT - I am trying to test the authentication using MockMvc but getting 'java.lang.AssertionError: Authentication should not be null' as well.

Comment From: davidgoate

Did anyone find a way around this without using mockmvc? I too am running into this issue.

Now I come to think about it, how does this work under the hood when using stateless mode and some sort of token based approach. I imagined that when setting the authentication on the SecurityContext perhaps some sort of thread local state was used for the remainder of the request and cleared at the end - or would that depend on a specific strategy set in the application.

Does mockmvc work in the same thread of execution - e.g. no HTTP request is actually made and the filter stack is invoked using the same thread as the test is being run with?

Comment From: msbober

Just hit the same problem testing stateless authentication with JWT. Maybe at least a documentation update so that it is clear that the matchers do not apply to the STATELESS scenario?

Comment From: bsautel

I am also facing this issue with Spring Boot 2.0.4.

I noticed that the tests don't always fail, they are unstable. In particular, I was using a very slow continuous integration server and I nearly never saw this issue. I am now using a new build server which is way faster than the previous one and the tests always fail.

I have many tests using MockMvc. Only one test class tests (and thus uses) the real authentication mechanism (BasicAuthenticationFilter and a custom filter for JWT). All the other test classes use SecurityMockMvcRequestPostProcessors.user() to bypass it.

On my laptop with IntelliJ, I also noticed that I encounter this issue if I launch only the test class using the real authentication mechanism. If I launch all the tests, they succeed (I have many tests using the same @SpringBootTest server instance). In this case, JUnit runs other tests before the instable one.

When the tests fail, I could notice the security context in which the authentication is set (BasicAuthenticationFilter:186) is not the same instance as the one used in the assertion to read the authentication (SecurityMockMvcResultMatchers:96).

This makes me think that this probably comes from a synchronization issue, as if the configuration of security contexts was done after the test server instance startup. This would explain why: 1. The tests fail on a fast server and succeed on a slow one 2. The tests fail if they run immediately after the test server startup and succeed if other tests executed before (using the same test server instance)

Hope it will help to fix this annoying issue!

Comment From: aSemy

I've created a workaround for this that I'm fairly sure is not thread-safe, but it works for now. (I'm using Kotlin, but the same should work in Java)

First I created a filter that will query SecurityContextHolder.getContext() in doFilter()

private class SecurityContextCaptorFilter : Filter {

    var securityContext: SecurityContext? = null
        private set

    override fun doFilter(
        request: ServletRequest?,
        response: ServletResponse?,
        chain: FilterChain?,
    ) {
        val context = SecurityContextHolder.getContext()
        if (context != null) {
            securityContext = context
        }
    }

    fun reset() {
        securityContext = null
    }
}

I created and registered this filter in the MockMvcBuilders DSL. I reset it, otherwise the result from a previous test might 'leak' into the next test.

class WebSecTest {

    private val securityContextCaptureFilter = SecurityContextCaptorFilter()

    @BeforeEach
    fun beforeEachTest() {

        securityContextCaptureFilter.reset()

        mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply<DefaultMockMvcBuilder>(springSecurity())
            .addFilters<DefaultMockMvcBuilder>(securityContextCaptureFilter)
            .build()
    }

// ...

Now in the test, I can query the captured value.

    @Test
    fun `test JWT is authorized`() {
        mvc
            .perform( ... )

        securityContextCaptureFilter.securityContext?.authentication?.name shouldBe "test-user"
    }

Full example

import io.kotest.matchers.shouldBe
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext

@SpringBootTest(
    classes = [Application::class],
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
)
class WebSecTest {

    private val securityContextCaptureFilter = SecurityContextCaptorFilter()

    @Autowired
    private lateinit var context: WebApplicationContext

    private lateinit var mvc: MockMvc

    @BeforeEach
    fun beforeEachTest() {
        mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply<DefaultMockMvcBuilder>(springSecurity())
            .addFilters<DefaultMockMvcBuilder>(securityContextCaptureFilter)
            .build()
    }

    @Test
    fun `test JWT is authorized`() {

        val testJwtToken = "some.valid.token"

        mvc
            .perform(
                get("users/get")
                    .header("Authorization", "Bearer $testJwtToken")
            ).andExpectAll(
                status().isOk,
                // FIXME: authentication is null because of stateless sessions
                // https://github.com/spring-projects/spring-security/issues/4516
                // authenticated()
                //     .withAuthenticationName("test-user"),
            )

        securityContextCaptureFilter.securityContext?.authentication?.name shouldBe "test-user"
    }
}

private class SecurityContextCaptorFilter : Filter {

    var securityContext: SecurityContext? = null
        private set

    override fun doFilter(
        request: ServletRequest?,
        response: ServletResponse?,
        chain: FilterChain?,
    ) {
        val context = SecurityContextHolder.getContext()
        if (context != null) {
            securityContext = context
        }
        if (chain != null && request != null && response != null) {
            chain.doFilter(request, response)
        }
    }
}