Testing Method Security

This section demonstrates how to use Spring Security’s Test support to test method-based security. We first introduce a MessageService that requires the user to be authenticated to be able to access it:

  • Java

  • Kotlin

public class HelloMessageService implements MessageService {

	@PreAuthorize("authenticated")
	public String getMessage() {
		Authentication authentication = SecurityContextHolder.getContext()
			.getAuthentication();
		return "Hello " + authentication;
	}
}
class HelloMessageService : MessageService {
    @PreAuthorize("authenticated")
    fun getMessage(): String {
        val authentication: Authentication = SecurityContextHolder.getContext().authentication
        return "Hello $authentication"
    }
}

The result of getMessage is a String that says “Hello” to the current Spring Security Authentication. The follwoing listing shows example output:

Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

Security Test Setup

Before we can use the Spring Security test support, we must perform some setup:

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
	// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
    // ...
}
1 @ExtendWith instructs the spring-test module that it should create an ApplicationContext. For additional information, refer to the Spring reference.
2 @ContextConfiguration instructs the spring-test the configuration to use to create the ApplicationContext. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the Spring Reference.

Spring Security hooks into Spring Test support through the WithSecurityContextTestExecutionListener, which ensures that our tests are run with the correct user. It does this by populating the SecurityContextHolder prior to running our tests. If you use reactive method security, you also need ReactorContextTestExecutionListener, which populates ReactiveSecurityContextHolder. After the test is done, it clears out the SecurityContextHolder. If you need only Spring Security related support, you can replace @ContextConfiguration with @SecurityTestExecutionListeners.

Remember, we added the @PreAuthorize annotation to our HelloMessageService, so it requires an authenticated user to invoke it. If we run the tests, we expect the following test will pass:

  • Java

  • Kotlin

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
	messageService.getMessage();
}
@Test(expected = AuthenticationCredentialsNotFoundException::class)
fun getMessageUnauthenticated() {
    messageService.getMessage()
}

@WithMockUser

The question is "How could we most easily run the test as a specific user?" The answer is to use @WithMockUser. The following test will be run as a user with the username "user", the password "password", and the roles "ROLE_USER".

  • Java

  • Kotlin

@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
@Test
@WithMockUser
fun getMessageWithMockUser() {
    val message: String = messageService.getMessage()
    // ...
}

Specifically the following is true:

  • The user with a username of user does not have to exist, since we mock the user object.

  • The Authentication that is populated in the SecurityContext is of type UsernamePasswordAuthenticationToken.

  • The principal on the Authentication is Spring Security’s User object.

  • The User has a username of user.

  • The User has a password of password.

  • A single GrantedAuthority named ROLE_USER is used.

The preceding example is handy, because it lets us use a lot of defaults. What if we wanted to run the test with a different username? The following test would run with a username of customUser (again, the user does not need to actually exist):

  • Java

  • Kotlin

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
	String message = messageService.getMessage();
...
}
@Test
@WithMockUser("customUsername")
fun getMessageWithMockUserCustomUsername() {
    val message: String = messageService.getMessage()
    // ...
}

We can also easily customize the roles. For example, the following test is invoked with a username of admin and roles of ROLE_USER and ROLE_ADMIN.

  • Java

  • Kotlin

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
	String message = messageService.getMessage();
	...
}
@Test
@WithMockUser(username="admin",roles=["USER","ADMIN"])
fun getMessageWithMockUserCustomUser() {
    val message: String = messageService.getMessage()
    // ...
}

If we do not want the value to automatically be prefixed with ROLE_ we can use the authorities attribute. For example, the following test is invoked with a username of admin and the USER and ADMIN authorities.

  • Java

  • Kotlin

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
	String message = messageService.getMessage();
	...
}
@Test
@WithMockUser(username = "admin", authorities = ["ADMIN", "USER"])
fun getMessageWithMockUserCustomUsername() {
    val message: String = messageService.getMessage()
    // ...
}

It can be a bit tedious to place the annotation on every test method. Instead, we can place the annotation at the class level. Then every test uses the specified user. The following example runs every test with a user whose username is admin, whose password is password, and who has the ROLE_USER and ROLE_ADMIN roles:

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
	// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles=["USER","ADMIN"])
class WithMockUserTests {
    // ...
}

If you use JUnit 5’s @Nested test support, you can also place the annotation on the enclosing class to apply to all nested classes. The following example runs every test with a user whose username is admin, whose password is password, and who has the ROLE_USER and ROLE_ADMIN roles for both test methods.

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {

	@Nested
	public class TestSuite1 {
		// ... all test methods use admin user
	}

	@Nested
	public class TestSuite2 {
		// ... all test methods use admin user
	}
}
@ExtendWith(SpringExtension::class)
@ContextConfiguration
@WithMockUser(username = "admin", roles = ["USER", "ADMIN"])
class WithMockUserTests {
    @Nested
    inner class TestSuite1 { // ... all test methods use admin user
    }

    @Nested
    inner class TestSuite2 { // ... all test methods use admin user
    }
}

By default, the SecurityContext is set during the TestExecutionListener.beforeTestMethod event. This is the equivalent of happening before JUnit’s @Before. You can change this to happen during the TestExecutionListener.beforeTestExecution event, which is after JUnit’s @Before but before the test method is invoked:

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithAnonymousUser

Using @WithAnonymousUser allows running as an anonymous user. This is especially convenient when you wish to run most of your tests with a specific user but want to run a few tests as an anonymous user. The following example runs withMockUser1 and withMockUser2 by using @WithMockUser and anonymous as an anonymous user:

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

	@Test
	public void withMockUser1() {
	}

	@Test
	public void withMockUser2() {
	}

	@Test
	@WithAnonymousUser
	public void anonymous() throws Exception {
		// override default to run as anonymous user
	}
}
@ExtendWith(SpringExtension.class)
@WithMockUser
class WithUserClassLevelAuthenticationTests {
    @Test
    fun withMockUser1() {
    }

    @Test
    fun withMockUser2() {
    }

    @Test
    @WithAnonymousUser
    fun anonymous() {
        // override default to run as anonymous user
    }
}

By default, the SecurityContext is set during the TestExecutionListener.beforeTestMethod event. This is the equivalent of happening before JUnit’s @Before. You can change this to happen during the TestExecutionListener.beforeTestExecution event, which is after JUnit’s @Before but before the test method is invoked:

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithUserDetails

While @WithMockUser is a convenient way to get started, it may not work in all instances. For example, some applications expect the Authentication principal to be of a specific type. This is done so that the application can refer to the principal as the custom type and reduce coupling on Spring Security.

The custom principal is often returned by a custom UserDetailsService that returns an object that implements both UserDetails and the custom type. For situations like this, it is useful to create the test user by using a custom UserDetailsService. That is exactly what @WithUserDetails does.

Assuming we have a UserDetailsService exposed as a bean, the following test is invoked with an Authentication of type UsernamePasswordAuthenticationToken and a principal that is returned from the UserDetailsService with the username of user:

  • Java

  • Kotlin

@Test
@WithUserDetails
public void getMessageWithUserDetails() {
	String message = messageService.getMessage();
	...
}
@Test
@WithUserDetails
fun getMessageWithUserDetails() {
    val message: String = messageService.getMessage()
    // ...
}

We can also customize the username used to lookup the user from our UserDetailsService. For example, this test can be run with a principal that is returned from the UserDetailsService with the username of customUsername:

  • Java

  • Kotlin

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
	String message = messageService.getMessage();
	...
}
@Test
@WithUserDetails("customUsername")
fun getMessageWithUserDetailsCustomUsername() {
    val message: String = messageService.getMessage()
    // ...
}

We can also provide an explicit bean name to look up the UserDetailsService. The following test looks up the username of customUsername by using the UserDetailsService with a bean name of myUserDetailsService:

  • Java

  • Kotlin

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
	String message = messageService.getMessage();
	...
}
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
fun getMessageWithUserDetailsServiceBeanName() {
    val message: String = messageService.getMessage()
    // ...
}

As we did with @WithMockUser, we can also place our annotation at the class level so that every test uses the same user. However, unlike @WithMockUser, @WithUserDetails requires the user to exist.

By default, the SecurityContext is set during the TestExecutionListener.beforeTestMethod event. This is the equivalent of happening before JUnit’s @Before. You can change this to happen during the TestExecutionListener.beforeTestExecution event, which is after JUnit’s @Before but before the test method is invoked:

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithSecurityContext

We have seen that @WithMockUser is an excellent choice if we do not use a custom Authentication principal. Next, we discovered that @WithUserDetails lets us use a custom UserDetailsService to create our Authentication principal but requires the user to exist. We now see an option that allows the most flexibility.

We can create our own annotation that uses the @WithSecurityContext to create any SecurityContext we want. For example, we might create an annotation named @WithMockCustomUser:

  • Java

  • Kotlin

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

	String username() default "rob";

	String name() default "Rob Winch";
}
@Retention(AnnotationRetention.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory::class)
annotation class WithMockCustomUser(val username: String = "rob", val name: String = "Rob Winch")

You can see that @WithMockCustomUser is annotated with the @WithSecurityContext annotation. This is what signals to Spring Security test support that we intend to create a SecurityContext for the test. The @WithSecurityContext annotation requires that we specify a SecurityContextFactory to create a new SecurityContext, given our @WithMockCustomUser annotation. The following listing shows our WithMockCustomUserSecurityContextFactory implementation:

  • Java

  • Kotlin

public class WithMockCustomUserSecurityContextFactory
	implements WithSecurityContextFactory<WithMockCustomUser> {
	@Override
	public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
		SecurityContext context = SecurityContextHolder.createEmptyContext();

		CustomUserDetails principal =
			new CustomUserDetails(customUser.name(), customUser.username());
		Authentication auth =
			UsernamePasswordAuthenticationToken.authenticated(principal, "password", principal.getAuthorities());
		context.setAuthentication(auth);
		return context;
	}
}
class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory<WithMockCustomUser> {
    override fun createSecurityContext(customUser: WithMockCustomUser): SecurityContext {
        val context = SecurityContextHolder.createEmptyContext()
        val principal = CustomUserDetails(customUser.name, customUser.username)
        val auth: Authentication =
            UsernamePasswordAuthenticationToken(principal, "password", principal.authorities)
        context.authentication = auth
        return context
    }
}

We can now annotate a test class or a test method with our new annotation and Spring Security’s WithSecurityContextTestExecutionListener to ensure that our SecurityContext is populated appropriately.

When creating your own WithSecurityContextFactory implementations, it is nice to know that they can be annotated with standard Spring annotations. For example, the WithUserDetailsSecurityContextFactory uses the @Autowired annotation to acquire the UserDetailsService:

  • Java

  • Kotlin

final class WithUserDetailsSecurityContextFactory
	implements WithSecurityContextFactory<WithUserDetails> {

	private UserDetailsService userDetailsService;

	@Autowired
	public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	public SecurityContext createSecurityContext(WithUserDetails withUser) {
		String username = withUser.value();
		Assert.hasLength(username, "value() must be non-empty String");
		UserDetails principal = userDetailsService.loadUserByUsername(username);
		Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(principal, principal.getPassword(), principal.getAuthorities());
		SecurityContext context = SecurityContextHolder.createEmptyContext();
		context.setAuthentication(authentication);
		return context;
	}
}
class WithUserDetailsSecurityContextFactory @Autowired constructor(private val userDetailsService: UserDetailsService) :
    WithSecurityContextFactory<WithUserDetails> {
    override fun createSecurityContext(withUser: WithUserDetails): SecurityContext {
        val username: String = withUser.value
        Assert.hasLength(username, "value() must be non-empty String")
        val principal = userDetailsService.loadUserByUsername(username)
        val authentication: Authentication =
            UsernamePasswordAuthenticationToken(principal, principal.password, principal.authorities)
        val context = SecurityContextHolder.createEmptyContext()
        context.authentication = authentication
        return context
    }
}

By default, the SecurityContext is set during the TestExecutionListener.beforeTestMethod event. This is the equivalent of happening before JUnit’s @Before. You can change this to happen during the TestExecutionListener.beforeTestExecution event, which is after JUnit’s @Before but before the test method is invoked:

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

Test Meta Annotations

If you reuse the same user within your tests often, it is not ideal to have to repeatedly specify the attributes. For example, if you have many tests related to an administrative user with a username of admin and roles of ROLE_USER and ROLE_ADMIN, you have to write:

  • Java

  • Kotlin

@WithMockUser(username="admin",roles={"USER","ADMIN"})
@WithMockUser(username="admin",roles=["USER","ADMIN"])

Rather than repeating this everywhere, we can use a meta annotation. For example, we could create a meta annotation named WithMockAdmin:

  • Java

  • Kotlin

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
@Retention(AnnotationRetention.RUNTIME)
@WithMockUser(value = "rob", roles = ["ADMIN"])
annotation class WithMockAdmin

Now we can use @WithMockAdmin in the same way as the more verbose @WithMockUser.

Meta annotations work with any of the testing annotations described above. For example, this means we could create a meta annotation for @WithUserDetails("admin") as well.