Symptom
- application extends org.springframework.security.core.userdetails.User class to store extra user attributes, for example CustomUser
- test case annonated with @WithMockUser in order to create mock user
- in some place of application, reading user attribute likes below
CustomUser user = (CustomUser) SecurityContextHolder.getContext().getAuthentication().getCredentials();
String name = user.getName();
When the test case run, following error shown
problem: cannot cast from User to CustomUser
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.ClassCastException: class org.springframework.security.core.userdetails.User cannot be cast to class CustomUser
Reason
When a test case starts, a org.springframework.security.core.userdetails.User instance is built by class org.springframework.security.test.context.support.WithMockUserSecurityContextFactory with properties specified by @WithMockUser annotation and then stored into SecurityContext. In some places of the application, object casting to custom user instances is performed in order to access the custom properties. Since instance needs casting is the parent class of custom user class, so casting operations will fail.
Solution
You need to modify @WithMockUser annotation class and mock user builder class org.springframework.security.test.context.support.WithMockUserSecurityContextFactory. Since these two classes are not extendable, you should create two new classes, copy code and then make necessary modifications. Following is the code that came from my example application.
package info.saladlam.example.spring.noticeboard.support;
import org.springframework.core.annotation.AliasFor;
import org.springframework.security.test.context.support.TestExecutionEvent;
import org.springframework.security.test.context.support.WithSecurityContext;
import java.lang.annotation.*;
/**
* Copy from org.springframework.security.test.context.support.WithMockUser and customize.
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithMockUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String value() default "user";
String username() default "";
String[] roles() default { "USER" };
String[] authorities() default {};
String password() default "password";
String name() default "";
String email() default "";
@AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
}
package info.saladlam.example.spring.noticeboard.support;
import info.saladlam.example.spring.noticeboard.entity.CustomUser;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithSecurityContextFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Copy from org.springframework.security.test.context.support.WithMockUserSecurityContextFactory and customize.
*/
final class WithMockUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser withUser) {
String username = StringUtils.hasLength(withUser.username()) ? withUser.username() : withUser.value();
Assert.notNull(username, () -> withUser + " cannot have null username on both username and value properties");
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : withUser.authorities()) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
}
if (grantedAuthorities.isEmpty()) {
for (String role : withUser.roles()) {
Assert.isTrue(!role.startsWith("ROLE_"), () -> "roles cannot start with ROLE_ Got " + role);
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
else if (!(withUser.roles().length == 1 && "USER".equals(withUser.roles()[0]))) {
throw new IllegalStateException("You cannot define roles attribute " + Arrays.asList(withUser.roles())
+ " with authorities attribute " + Arrays.asList(withUser.authorities()));
}
User principal = new CustomUser(username, withUser.password(), true, true, true, true, grantedAuthorities, withUser.name(), withUser.email());
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(),
principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
And then when writing the test case, your new annotation should be used.
@WithMockCustomUser(username = "user1", authorities = {"USER"}, name = "First Last")
public void userAction1() throws Exception {
...
}