{"record":{"id":"b504e4765aede88e","repo":"quarkusio/quarkus","slug":"expected-claim-s-must-be-a-list-of-strings-or-a-s","errorCode":null,"errorMessage":"expected claim %s must be a list of strings or a string","messagePattern":"expected claim (.+?) must be a list of strings or a string","errorType":"validation","errorClass":"org.jose4j.jwt.MalformedClaimException","httpStatus":null,"severity":"error","filePath":"extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java","lineNumber":757,"sourceCode":"                if (requiredClaimValues.size() == 1) {\n                    String actualClaimValue = claims.getStringClaimValue(requiredClaimName);\n                    String requiredClaimValue = requiredClaimValues.iterator().next();\n                    if (!requiredClaimValue.equals(actualClaimValue)) {\n                        return \"claim \" + requiredClaimName + \" does not match expected value of \" + requiredClaimValues;\n                    }\n                } else {\n                    throw new MalformedClaimException(\"expected claim \" + requiredClaimName + \" must be a list of strings\");\n                }\n            } else {\n                if (claims.isClaimValueStringList(requiredClaimName)) {\n                    List<String> actualClaimValues = claims.getStringListClaimValue(requiredClaimName);\n                    for (String requiredClaimValue : requiredClaimValues) {\n                        if (!actualClaimValues.contains(requiredClaimValue)) {\n                            return \"claim \" + requiredClaimName + \" does not match expected value of \" + requiredClaimValues;\n                        }\n                    }\n                } else {\n                    throw new MalformedClaimException(\n                            \"expected claim \" + requiredClaimName + \" must be a list of strings or a string\");\n                }\n            }\n            return null;\n        }\n    }\n\n    private static Map<String, Object> tokenMap(String token, TokenType tokenType) {\n        return Map.of(tokenType == TokenType.ID_TOKEN ? OidcConstants.ID_TOKEN_VALUE : OidcConstants.ACCESS_TOKEN_VALUE,\n                token);\n    }\n\n    private static final class CatchingErrorCodeValidator extends ErrorCodeValidatorAdapter {\n\n        private AuthenticationFailedException authenticationFailure;\n\n        private CatchingErrorCodeValidator(Validator validator) {\n            super(validator);","sourceCodeStart":739,"sourceCodeEnd":775,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java#L739-L775","documentation":"Thrown by OidcProvider's CustomClaimsValidator when a required claim configured in quarkus.oidc.token.required-claims is present in the token but is neither a string nor a list of strings (e.g. a number, boolean, or object). Only string and string-list claims can be validated against the configured expected values, so MalformedClaimException is thrown.","triggerScenarios":"quarkus.oidc.token.required-claims.<name> configured for a claim that the token carries as a non-string JSON type — for example \"loa\": 2 (number) or \"roles\": {\"admin\": true} (object).","commonSituations":"Identity providers emitting numeric authentication levels or structured claims that developers reference in required-claims; copy-pasting claim names from decoded JSON without noticing the value type differs from what's configured.","solutions":["Reference only string-valued claims in required-claims (decode the token to check the JSON type)","If the claim is a number/boolean, implement a custom Validator bean instead of using required-claims config","Ask the issuer to emit the claim as a string (e.g. \"loa\": \"2\" instead of 2)","Verify the claim name isn't accidentally colliding with a different claim of non-string type"],"exampleFix":"// token: \"loa\": 2 (number)\n// before: quarkus.oidc.token.required-claims.loa=2 -> throws\n// after: custom Validator bean:\npublic class LoaValidator implements Validator {\n    public Error validate(JwtContext ctx) {\n        return ctx.getJwtClaims().getClaimValue(\"loa\", Integer.class) >= 2 ? null : Error.authFailure(\"loa too low\");\n    }\n}","handlingStrategy":"type-guard","validationCode":"// ensure the claim value type matches what required-claims supports\nObject v = decodedClaims.get(claimName);\nif (v != null && !(v instanceof String) && !(v instanceof List)) {\n    // non-string claim (number/boolean/object): use a custom Validator instead\n}","typeGuard":"boolean isValidatableClaim(Object v) {\n    return v instanceof String\n        || (v instanceof List<?> l && l.stream().allMatch(String.class::isInstance));\n}","tryCatchPattern":null,"preventionTips":["Only reference string/string-list claims in required-claims","Check claim types by decoding a production token, not by claim name alone","Convert numeric/boolean claims to strings at the issuer or validate them in code"],"tags":["oidc","jwt-claims","configuration","validation"],"backgroundTag":"claim-type-mismatch","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}