apereo/cas · error · CredentialNotFoundException

Missing surrogate username in credential

Error message

Missing surrogate username in credential

What it means

SurrogateAuthenticationPostProcessor runs after primary authentication to enforce surrogate eligibility. It reads the SurrogateCredentialTrait from the credential metadata; if the trait is present but the surrogate username inside it is blank, it throws CredentialNotFoundException because there is no target user to impersonate.

Solutions

  1. Correct the submitted surrogate syntax so it contains a non-blank target user (e.g. userA+userB).
  2. Verify the SurrogateCredentialTrait is populated from the correct request parameter/attribute in the webflow.
  3. Check any custom credential/trait extraction code for blank-string handling.
  4. Test with a direct, well-formed surrogate identifier to isolate where the value is lost.

Example fix

// before: malformed surrogate id
userB+
// after
userA+userB
Defensive patterns

Strategy: validation

Validate before calling

// before invoking the processor, ensure the trait carries a non-blank user
String surrogate = Optional.ofNullable(credential.getCredentialMetadata()
        .getTrait(SurrogateCredentialTrait.class))
    .map(SurrogateCredentialTrait::getSurrogateUsername)
    .orElse("");
if (surrogate.isBlank()) throw new IllegalArgumentException("surrogate username required");

Type guard

function hasSurrogateUsername(cred) {
  const trait = cred?.credentialMetadata?.getTrait?.(SurrogateCredentialTrait);
  return typeof trait?.surrogateUsername === 'string' && trait.surrogateUsername.trim() !== '';
}

Try / catch

try {
    processor.process(transaction, result);
} catch (CredentialNotFoundException e) {
    // redirect user back to the surrogate selection step
    LOGGER.warn("Surrogate username missing from credential");
}

Prevention

When it happens

Trigger: A credential was flagged as a surrogate credential (trait present) but extractSurrogateUser produced an empty string — e.g. the 'username+surrogate' syntax was malformed (trailing '+', only '+'), or the webflow submitted an empty surrogate field.

Common situations: Malformed surrogate username in the login form or URL; custom credential wrapping that strips the surrogate part; webflow misconfiguration losing the surrogate parameter before the trait is populated.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-surrogate-core/src/main/java/org/apereo/cas/authentication/SurrogateAuthenticationPostProcessor.java:58

        val principal = authentication.getPrincipal();

        if (!(principal instanceof final SurrogatePrincipal primaryPrincipal)) {
            LOGGER.trace("Provided principal is one intended for surrogate authentication");
            return;
        }
        val primaryCredential = transaction.getPrimaryCredential();
        if (primaryCredential.isEmpty()) {
            throw new AuthenticationException("Unable to determine primary credentials");
        }
        val surrogateUsername = primaryCredential.get().getCredentialMetadata()
            .getTrait(SurrogateCredentialTrait.class)
            .map(SurrogateCredentialTrait::getSurrogateUsername)
            .orElseThrow(() -> new AuthenticationException("Unable to determine surrogate credential"));

        try {
            if (StringUtils.isBlank(surrogateUsername)) {
                LOGGER.error("No surrogate username was specified as part of the credential");
                throw new CredentialNotFoundException("Missing surrogate username in credential");
            }
            LOGGER.debug("Authenticated [{}] will be checked for surrogate eligibility next for [{}]...", primaryPrincipal, surrogateUsername);
            if (transaction.getService() != null) {
                val svc = servicesManager.findServiceBy(transaction.getService());

                val serviceAccessAudit = AuditableContext.builder()
                    .service(transaction.getService())
                    .authentication(authentication)
                    .registeredService(svc)
                    .build();

                val accessResult = registeredServiceAccessStrategyEnforcer.execute(serviceAccessAudit);
                accessResult.throwExceptionIfNeeded();
            }

            if (surrogateAuthenticationService.canImpersonate(surrogateUsername, primaryPrincipal.getPrimary(), Optional.ofNullable(transaction.getService()))) {
                LOGGER.debug("Principal [{}] is authorized to authenticate as [{}]", primaryPrincipal, surrogateUsername);
                publishSuccessEvent(primaryPrincipal, surrogateUsername);

View on GitHub (pinned to e7288fc434)