123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696 |
- [[test-method]]
- = 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 in order to access it.
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- public class HelloMessageService implements MessageService {
- @PreAuthorize("authenticated")
- public String getMessage() {
- Authentication authentication = SecurityContextHolder.getContext()
- .getAuthentication();
- return "Hello " + authentication;
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- class HelloMessageService : MessageService {
- @PreAuthorize("authenticated")
- fun getMessage(): String {
- val authentication: Authentication = SecurityContextHolder.getContext().authentication
- return "Hello $authentication"
- }
- }
- ----
- ======
- The result of `getMessage` is a String saying "Hello" to the current Spring Security `Authentication`.
- An example of the output is displayed below.
- [source,text]
- ----
- 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
- ----
- [[test-method-setup]]
- == Security Test Setup
- Before we can use Spring Security Test support, we must perform some setup. An example can be seen below:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @ExtendWith(SpringExtension.class) // <1>
- @ContextConfiguration // <2>
- public class WithMockUserTests {
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @ExtendWith(SpringExtension.class)
- @ContextConfiguration
- class WithMockUserTests {
- ----
- ======
- This is a basic example of how to setup Spring Security Test. The highlights are:
- <1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information, refer to the {spring-framework-reference-url}testing.html#testcontext-junit-jupiter-extension[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-framework-reference-url}testing.html#spring-testing-annotation-contextconfiguration[Spring Reference]
- NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user.
- It does this by populating the `SecurityContextHolder` prior to running our tests.
- If you are using reactive method security, you will also need `ReactorContextTestExecutionListener` which populates `ReactiveSecurityContextHolder`.
- After the test is done, it will clear out the `SecurityContextHolder`.
- If you only need Spring Security related support, you can replace `@ContextConfiguration` with `@SecurityTestExecutionListeners`.
- Remember we added the `@PreAuthorize` annotation to our `HelloMessageService` and so it requires an authenticated user to invoke it.
- If we ran the following test, we would expect the following test will pass:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test(expected = AuthenticationCredentialsNotFoundException.class)
- public void getMessageUnauthenticated() {
- messageService.getMessage();
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Test(expected = AuthenticationCredentialsNotFoundException::class)
- fun getMessageUnauthenticated() {
- messageService.getMessage()
- }
- ----
- ======
- [[test-method-withmockuser]]
- == @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".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithMockUser
- public void getMessageWithMockUser() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Test
- @WithMockUser
- fun getMessageWithMockUser() {
- val message: String = messageService.getMessage()
- // ...
- }
- ----
- ======
- Specifically the following is true:
- * The user with the username "user" does not have to exist since we are mocking the user
- * 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` will have the username of "user", the password "password", and a single `GrantedAuthority` named "ROLE_USER" is used.
- Our example is nice because we are able to leverage a lot of defaults.
- What if we wanted to run the test with a different username?
- The following test would run with the username "customUser". Again, the user does not need to actually exist.
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithMockUser("customUsername")
- public void getMessageWithMockUserCustomUsername() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Test
- @WithMockUser("customUsername")
- fun getMessageWithMockUserCustomUsername() {
- val message: String = messageService.getMessage()
- // ...
- }
- ----
- ======
- We can also easily customize the roles.
- For example, this test will be invoked with the username "admin" and the roles "ROLE_USER" and "ROLE_ADMIN".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithMockUser(username="admin",roles={"USER","ADMIN"})
- public void getMessageWithMockUserCustomUser() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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 leverage the authorities attribute.
- For example, this test will be invoked with the username "admin" and the authorities "USER" and "ADMIN".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
- public void getMessageWithMockUserCustomAuthorities() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Test
- @WithMockUser(username = "admin", authorities = ["ADMIN", "USER"])
- fun getMessageWithMockUserCustomUsername() {
- val message: String = messageService.getMessage()
- // ...
- }
- ----
- ======
- Of course it can be a bit tedious placing the annotation on every test method.
- Instead, we can place the annotation at the class level and every test will use the specified user.
- For example, the following would run every test with a user with the username "admin", the password "password", and the roles "ROLE_USER" and "ROLE_ADMIN".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @ExtendWith(SpringExtension.class)
- @ContextConfiguration
- @WithMockUser(username="admin",roles={"USER","ADMIN"})
- public class WithMockUserTests {
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @ExtendWith(SpringExtension.class)
- @ContextConfiguration
- @WithMockUser(username="admin",roles=["USER","ADMIN"])
- class WithMockUserTests {
- ----
- ======
- If you are using JUnit 5's `@Nested` test support, you can also place the annotation on the enclosing class to apply to all nested classes.
- For example, the following would run every test with a user with the username "admin", the password "password", and the roles "ROLE_USER" and "ROLE_ADMIN" for both test methods.
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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.
- [source,java]
- ----
- @WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
- ----
- [[test-method-withanonymoususer]]
- == @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.
- For example, the following will run withMockUser1 and withMockUser2 using <<test-method-withmockuser,@WithMockUser>> and anonymous as an anonymous user.
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @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
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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.
- [source,java]
- ----
- @WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
- ----
- [[test-method-withuserdetails]]
- == @WithUserDetails
- While `@WithMockUser` is a very convenient way to get started, it may not work in all instances.
- For example, it is common for applications to expect that the `Authentication` principal 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 times 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 using the custom `UserDetailsService`.
- That is exactly what `@WithUserDetails` does.
- Assuming we have a `UserDetailsService` exposed as a bean, the following test will be invoked with an `Authentication` of type `UsernamePasswordAuthenticationToken` and a principal that is returned from the `UserDetailsService` with the username of "user".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithUserDetails
- public void getMessageWithUserDetails() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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 would be run with a principal that is returned from the `UserDetailsService` with the username of "customUsername".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithUserDetails("customUsername")
- public void getMessageWithUserDetailsCustomUsername() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Test
- @WithUserDetails("customUsername")
- fun getMessageWithUserDetailsCustomUsername() {
- val message: String = messageService.getMessage()
- // ...
- }
- ----
- ======
- We can also provide an explicit bean name to look up the `UserDetailsService`.
- For example, this test would look up the username of "customUsername" using the `UserDetailsService` with the bean name "myUserDetailsService".
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Test
- @WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
- public void getMessageWithUserDetailsServiceBeanName() {
- String message = messageService.getMessage();
- ...
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @Test
- @WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
- fun getMessageWithUserDetailsServiceBeanName() {
- val message: String = messageService.getMessage()
- // ...
- }
- ----
- ======
- Like `@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.
- [source,java]
- ----
- @WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
- ----
- [[test-method-withsecuritycontext]]
- == @WithSecurityContext
- We have seen that `@WithMockUser` is an excellent choice if we are not using a custom `Authentication` principal.
- Next we discovered that `@WithUserDetails` would allow us to use a custom `UserDetailsService` to create our `Authentication` principal but required the user to exist.
- We will 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` as shown below:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Retention(RetentionPolicy.RUNTIME)
- @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
- public @interface WithMockCustomUser {
- String username() default "rob";
- String name() default "Rob Winch";
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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 we specify a `SecurityContextFactory` that will create a new `SecurityContext` given our `@WithMockCustomUser` annotation.
- You can find our `WithMockCustomUserSecurityContextFactory` implementation below:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- 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;
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- 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` will 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`:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- 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;
- }
- }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- 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.
- [source,java]
- ----
- @WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
- ----
- [[test-method-meta-annotations]]
- == 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 there are many tests related to an administrative user with the username "admin" and the roles `ROLE_USER` and `ROLE_ADMIN` you would have to write:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @WithMockUser(username="admin",roles={"USER","ADMIN"})
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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`:
- [tabs]
- ======
- Java::
- +
- [source,java,role="primary"]
- ----
- @Retention(RetentionPolicy.RUNTIME)
- @WithMockUser(value="rob",roles="ADMIN")
- public @interface WithMockAdmin { }
- ----
- Kotlin::
- +
- [source,kotlin,role="secondary"]
- ----
- @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.
|