DEV Community

Salad Lam
Salad Lam

Posted on

Error on test case annotated with @WithMockUser on application extend Spring Security User class

Notice

I wrote this article and was originally published on Qiita on 19 September 2022.


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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;

}
Enter fullscreen mode Exit fullscreen mode
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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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 {
        ...
    }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)