{"record":{"id":"db520503f642ff15","repo":"spring-projects/spring-security","slug":"principal-is-not-assignable-to-parameter-g-db5205","errorCode":null,"errorMessage":"principal + \" is not assignable to \" + parameter.getParameterType()","messagePattern":"principal \\+ \" is not assignable to \" \\+ parameter\\.getParameterType\\(\\)","errorType":"exception","errorClass":"ClassCastException","httpStatus":null,"severity":"error","filePath":"web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java","lineNumber":135,"sourceCode":"\t\tAuthentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();\n\t\tif (authentication == null) {\n\t\t\treturn null;\n\t\t}\n\t\tObject principal = authentication.getPrincipal();\n\t\tAuthenticationPrincipal annotation = findMethodAnnotation(parameter);\n\t\tAssert.notNull(annotation, \"@AuthenticationPrincipal is required. Call supportsParameter first.\");\n\t\tString expressionToParse = annotation.expression();\n\t\tif (StringUtils.hasLength(expressionToParse)) {\n\t\t\tStandardEvaluationContext context = new StandardEvaluationContext();\n\t\t\tcontext.setRootObject(principal);\n\t\t\tcontext.setVariable(\"this\", principal);\n\t\t\tcontext.setBeanResolver(this.beanResolver);\n\t\t\tExpression expression = this.parser.parseExpression(expressionToParse);\n\t\t\tprincipal = expression.getValue(context);\n\t\t}\n\t\tif (principal != null && !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {\n\t\t\tif (annotation.errorOnInvalidType()) {\n\t\t\t\tthrow new ClassCastException(principal + \" is not assignable to \" + parameter.getParameterType());\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\treturn principal;\n\t}\n\n\t@Override\n\tpublic boolean supportsParameter(MethodParameter parameter) {\n\t\treturn findMethodAnnotation(parameter) != null;\n\t}\n\n\t/**\n\t * Sets the {@link BeanResolver} to be used on the expressions.\n\t * @param beanResolver the {@link BeanResolver} to use\n\t */\n\tpublic void setBeanResolver(BeanResolver beanResolver) {\n\t\tthis.beanResolver = beanResolver;\n\t}","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java#L117-L153","documentation":"The current org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver resolves @AuthenticationPrincipal parameters, optionally applying a SpEL expression to the principal. If the resulting principal (raw or expression-evaluated) is not assignable to the declared parameter type and the annotation's errorOnInvalidType is true, resolveArgument throws a ClassCastException naming the principal and the target parameter type.","triggerScenarios":"A controller declares @AuthenticationPrincipal(expression = \"...\") CustomType param (or plain @AuthenticationPrincipal CustomType) with errorOnInvalidType=true, but the SecurityContext principal — or the SpEL expression's result — is of an unrelated runtime type, so ClassUtils.isAssignable fails.","commonSituations":"SpEL expressions returning the wrong property type (e.g. expression yielding a String while the parameter expects a Long or custom type); principal type changed after migrating from session login to JWT/OAuth2; method-security or tests injecting a different Authentication; copy-pasted controllers across apps with different UserDetails classes.","solutions":["Verify the expression's evaluated type matches the parameter type (e.g. principal.claims['sub'] returns String vs Long) and correct the expression or parameter declaration.","Remove errorOnInvalidType=true so a mismatch yields null instead of a 500 ClassCastException, and handle null explicitly.","Unify principal types across all authentication mechanisms (custom UserDetails adopted by OAuth2/JWT login too).","Add a test that authenticates through the real mechanism and invokes the controller to catch type drift early."],"exampleFix":"// before\npublic OrderDto get(@AuthenticationPrincipal(errorOnInvalidType = true, expression = \"attributes['sub']\") MyUser user)\n// after (expression returns a String, so accept String)\npublic OrderDto get(@AuthenticationPrincipal(expression = \"attributes['sub']\") String userId) { ... }","handlingStrategy":"type-guard","validationCode":"Authentication auth = SecurityContextHolder.getContext().getAuthentication();\nObject principal = auth == null ? null : auth.getPrincipal();\nif (principal != null && !ClassUtils.isAssignable(MyUser.class, principal.getClass())) {\n    throw new AccessDeniedException(\"Principal \" + principal.getClass() + \" is not MyUser\");\n}\n","typeGuard":"static <T> T principalAs(Class<T> type) {\n    Authentication a = SecurityContextHolder.getContext().getAuthentication();\n    Object p = a == null ? null : a.getPrincipal();\n    return type.isInstance(p) ? type.cast(p) : null;\n}","tryCatchPattern":"try {\n    return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);\n} catch (ClassCastException e) {\n    log.warn(\"@AuthenticationPrincipal type mismatch (check expression result type): {}\", e.getMessage());\n    return null; // or map to 403 depending on policy\n}","preventionTips":["Check the SpEL expression's return type matches the declared parameter type","Migrate carefully when switching to JWT/OAuth2 — principal type changes from UserDetails to String/Jwt","Leave errorOnInvalidType at its default (false) unless a hard failure is required","Write controller tests that authenticate via the real security config to catch type drift"],"tags":["spring-security","spring-mvc","spel","classcastexception","type-mismatch"],"backgroundTag":"type-mismatch","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}