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)
}
}
}