LoginSignup
0
0

More than 1 year has passed since last update.

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

Posted at

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.

WithMockCustomUser.java
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;

}
WithMockUserSecurityContextFactory.java
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 {
        ...
    }
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0