spring-projects/spring-security · error · ClassCastException

principal + " is not assignable to " + parameter.getParamete

Error message

principal + " is not assignable to " + parameter.getParameterType()

What it means

The legacy org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver resolves @AuthenticationPrincipal controller parameters. When the authenticated principal's runtime type is not assignable to the declared parameter type and the annotation sets errorOnInvalidType=true, resolveArgument throws a ClassCastException naming the principal and target type; otherwise it quietly returns null.

Source

Thrown at web/src/main/java/org/springframework/security/web/bind/support/AuthenticationPrincipalArgumentResolver.java:108

public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
	}

	@Override
	public @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication == null) {
			return null;
		}
		Object principal = authentication.getPrincipal();
		if (principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) {
			@Nullable AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
			if (authPrincipal != null && authPrincipal.errorOnInvalidType()) {
				throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType());
			}
			return null;
		}
		return principal;
	}

	/**
	 * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}.
	 * @param annotationClass the class of the {@link Annotation} to find on the
	 * {@link MethodParameter}
	 * @param parameter the {@link MethodParameter} to search for an {@link Annotation}
	 * @return the {@link Annotation} that was found or null.
	 */
	private <T extends Annotation> @Nullable T findMethodAnnotation(Class<T> annotationClass,
			MethodParameter parameter) {
		T annotation = parameter.getParameterAnnotation(annotationClass);
		if (annotation != null) {
			return annotation;

View on GitHub (pinned to 96852e8860)

Solutions

  1. Ensure every authentication path in the app populates a SecurityContext whose principal implements/extends the controller's declared parameter type (use one shared UserDetails implementation).
  2. If the parameter should simply be empty on mismatch, remove errorOnInvalidType=true (default behavior returns null instead of throwing).
  3. Align test security setup so mock Authentication objects use the same principal type production code produces.
  4. Alternatively declare the parameter as Object or the common supertype and narrow manually with instanceof.

Example fix

// before
@GetMapping("/me")
public MeDto me(@AuthenticationPrincipal(errorOnInvalidType = true) MyUser user) { ... }
// after
@GetMapping("/me")
public MeDto me(@AuthenticationPrincipal MyUser user) { // null when type differs
    if (user == null) throw new AccessDeniedException("unexpected principal type");
    ...
}
Defensive patterns

Strategy: type-guard

Validate before calling

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof MyUser)) {
    throw new AccessDeniedException("Expected principal of type MyUser, got "
        + (auth == null ? "anonymous" : auth.getPrincipal().getClass().getSimpleName()));
}

Type guard

boolean hasPrincipalOfType(Class<?> expected) {
    Authentication a = SecurityContextHolder.getContext().getAuthentication();
    return a != null && expected.isInstance(a.getPrincipal());
}

Try / catch

try {
    return resolver.resolveArgument(parameter, null, webRequest, binderFactory);
} catch (ClassCastException e) {
    throw new AccessDeniedException("Principal type mismatch for @AuthenticationPrincipal: " + e.getMessage());
}

Prevention

When it happens

Trigger: A controller method declares @AuthenticationPrincipal CustomUserDetails user, but the SecurityContext holds a different principal type (e.g. String username when no UserDetailsService is used, or a different UserDetails implementation from another auth mechanism) — with errorOnInvalidType=true on the annotation.

Common situations: Mixing authentication mechanisms (form login with one UserDetails type, OAuth/JWT login producing a different principal); switching to JWT where the principal becomes a String or Jwt; multiple UserDetailsService beans registered; tests that stub Authentication with the wrong principal type.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/76668c3d8e655b81. Report an issue: GitHub.