apereo/cas · error · IllegalArgumentException

Missing required principal attribute for claim

Error message

Missing required principal attribute for claim %s

What it means

Thrown by BaseOidcVerifiableCredentialEncoder.produceClaims when a claim configured as mandatory in the verifiable-credential configuration has no corresponding attribute on the authenticated principal. The encoder iterates configuration.getClaims(), looks each claim up in principal.getAttributes(), and fails fast if a required attribute is absent rather than emitting a credential with missing required claims.

Solutions

  1. Ensure the principal actually carries an attribute whose key exactly matches the configured claim name (check attribute repository, release policy, and casing).
  2. Set the claim's mandatory flag to false in the VC configuration if the attribute is genuinely optional.
  3. Debug the resolved principal attributes at authentication time and add the missing attribute to the returned attribute set.
  4. Fix the claim name in configuration.getClaims() to match an existing attribute key.

Example fix

// before
claims: { "email": { mandatory: true } }  // principal has no 'email' attribute
// after
claims: { "email": { mandatory: false } }  // or populate/release the email attribute
Defensive patterns

Strategy: validation

Validate before calling

config.getClaims().forEach((claim, props) -> {
    if (props.isMandatory() && !principal.getAttributes().containsKey(claim)) {
        throw new IllegalArgumentException("Missing mandatory attribute: " + claim);
    }
});

Try / catch

try {
    encoder.produceClaims(principal, ...);
} catch (IllegalArgumentException e) {
    log.error("VC claim validation failed: {}", e.getMessage());
}

Prevention

When it happens

Trigger: Issuing a verifiable credential whose configured claim is marked mandatory=true (claimProps.isMandatory()) while the authenticated principal lacks an attribute with exactly that name; attribute name casing mismatch between the claim config and the released attribute.

Common situations: Admins configure cas.authn.oidc.vc claim mappings referencing attributes not enabled in the attribute release policy or not returned by the underlying attribute repository (LDAP/DB missing the field); renamed attributes in the identity source; case-sensitivity differences between claim name and attribute key.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/6930730eec102b65. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/oidc/vc/issuer/enc/BaseOidcVerifiableCredentialEncoder.java:40

 */
@RequiredArgsConstructor
public abstract class BaseOidcVerifiableCredentialEncoder implements OidcVerifiableCredentialEncoder {
    protected static final int CLAIM_VALIDITY_IN_MINUTES = 5;

    protected final OidcConfigurationContext configurationContext;

    protected Map<String, Object> produceClaims(final Principal principal, final OidcVerifiableCredentialValidationContext context) {
        val properties = configurationContext.getCasProperties().getAuthn().getOidc().getVc();
        val configurationId = context.resolveConfigurationId();
        val configuration = properties.getIssuer().getCredentialConfigurations().get(configurationId);
        Objects.requireNonNull(configuration, () -> "Unable to locate credential configuration " + configurationId);
        val claims = new LinkedHashMap<String, Object>();

        configuration.getClaims().forEach((claimName, claimProps) -> {
            val rawValue = principal.getAttributes().get(claimName);

            if (rawValue == null && claimProps.isMandatory()) {
                throw new IllegalArgumentException("Missing required principal attribute for claim %s".formatted(claimName));
            }
            if (rawValue != null) {
                val claimValue = rawValue.size() == 1 ? rawValue.getFirst() : rawValue;
                claims.put(claimName, !(claimValue instanceof Number) && NumberUtils.isParsable(claimValue.toString())
                    ? NumberUtils.createNumber(claimValue.toString())
                    : claimValue);
            }
        });
        return claims;
    }

    protected OidcVerifiableCredentialConfigurationProperties resolveConfiguration(final String configurationId) {
        val properties = configurationContext.getCasProperties().getAuthn().getOidc().getVc();
        val configuration = properties.getIssuer().getCredentialConfigurations().get(configurationId);
        Objects.requireNonNull(configuration, () -> "Unable to locate credential configuration " + configurationId);
        return configuration;
    }

View on GitHub (pinned to e7288fc434)