{"record":{"id":"012fd75c911d0329","repo":"apereo/cas","slug":"missing-surrogate-username-in-credential","errorCode":null,"errorMessage":"Missing surrogate username in credential","messagePattern":"Missing surrogate username in credential","errorType":"validation","errorClass":"CredentialNotFoundException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-surrogate-core/src/main/java/org/apereo/cas/authentication/SurrogateAuthenticationPostProcessor.java","lineNumber":58,"sourceCode":"        val principal = authentication.getPrincipal();\n\n        if (!(principal instanceof final SurrogatePrincipal primaryPrincipal)) {\n            LOGGER.trace(\"Provided principal is one intended for surrogate authentication\");\n            return;\n        }\n        val primaryCredential = transaction.getPrimaryCredential();\n        if (primaryCredential.isEmpty()) {\n            throw new AuthenticationException(\"Unable to determine primary credentials\");\n        }\n        val surrogateUsername = primaryCredential.get().getCredentialMetadata()\n            .getTrait(SurrogateCredentialTrait.class)\n            .map(SurrogateCredentialTrait::getSurrogateUsername)\n            .orElseThrow(() -> new AuthenticationException(\"Unable to determine surrogate credential\"));\n\n        try {\n            if (StringUtils.isBlank(surrogateUsername)) {\n                LOGGER.error(\"No surrogate username was specified as part of the credential\");\n                throw new CredentialNotFoundException(\"Missing surrogate username in credential\");\n            }\n            LOGGER.debug(\"Authenticated [{}] will be checked for surrogate eligibility next for [{}]...\", primaryPrincipal, surrogateUsername);\n            if (transaction.getService() != null) {\n                val svc = servicesManager.findServiceBy(transaction.getService());\n\n                val serviceAccessAudit = AuditableContext.builder()\n                    .service(transaction.getService())\n                    .authentication(authentication)\n                    .registeredService(svc)\n                    .build();\n\n                val accessResult = registeredServiceAccessStrategyEnforcer.execute(serviceAccessAudit);\n                accessResult.throwExceptionIfNeeded();\n            }\n\n            if (surrogateAuthenticationService.canImpersonate(surrogateUsername, primaryPrincipal.getPrimary(), Optional.ofNullable(transaction.getService()))) {\n                LOGGER.debug(\"Principal [{}] is authorized to authenticate as [{}]\", primaryPrincipal, surrogateUsername);\n                publishSuccessEvent(primaryPrincipal, surrogateUsername);","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-surrogate-core/src/main/java/org/apereo/cas/authentication/SurrogateAuthenticationPostProcessor.java#L40-L76","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Correct the submitted surrogate syntax so it contains a non-blank target user (e.g. userA+userB).","Verify the SurrogateCredentialTrait is populated from the correct request parameter/attribute in the webflow.","Check any custom credential/trait extraction code for blank-string handling.","Test with a direct, well-formed surrogate identifier to isolate where the value is lost."],"exampleFix":"// before: malformed surrogate id\nuserB+\n// after\nuserA+userB","handlingStrategy":"validation","validationCode":"// before invoking the processor, ensure the trait carries a non-blank user\nString surrogate = Optional.ofNullable(credential.getCredentialMetadata()\n        .getTrait(SurrogateCredentialTrait.class))\n    .map(SurrogateCredentialTrait::getSurrogateUsername)\n    .orElse(\"\");\nif (surrogate.isBlank()) throw new IllegalArgumentException(\"surrogate username required\");","typeGuard":"function hasSurrogateUsername(cred) {\n  const trait = cred?.credentialMetadata?.getTrait?.(SurrogateCredentialTrait);\n  return typeof trait?.surrogateUsername === 'string' && trait.surrogateUsername.trim() !== '';\n}","tryCatchPattern":"try {\n    processor.process(transaction, result);\n} catch (CredentialNotFoundException e) {\n    // redirect user back to the surrogate selection step\n    LOGGER.warn(\"Surrogate username missing from credential\");\n}","preventionTips":["Validate the 'userA+userB' syntax in the login/webflow UI","Unit-test credential trait population for surrogate flows","Never allow blank surrogate fields to reach authentication post-processing"],"tags":["surrogate","impersonation","missing-value"],"backgroundTag":"missing-credentials","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}