apereo/cas · error · FailedLoginException

Unable to detect the authentication principal for

Error message

Unable to detect the authentication principal for ${username}

What it means

RestAuthenticationHandler throws this FailedLoginException from buildPrincipalFromResponse when the successful (HTTP 200) REST response body cannot be turned into a CAS Principal — typically because the JSON does not deserialize into a Principal (missing/invalid id field) or parsing throws. Any Throwable in the parsing path is logged via LoggingUtils and converted into this failed login.

Solutions

  1. Inspect the actual response body (curl the endpoint) and confirm it is valid JSON with an id and attributes matching CAS Principal format
  2. Add the Jackson @class type info or align the endpoint payload with the expected Principal shape
  3. Check CAS logs above this message for the underlying deserialization exception to pinpoint the field
  4. If the endpoint returns 200 with an empty body for unknown users, fix the endpoint to return 404 instead

Example fix

// before (endpoint response)
{"name":"jdoe"}
// after (valid CAS Principal payload)
{"@class":"org.apereo.cas.authentication.principal.SimplePrincipal","id":"jdoe","attributes":{"email":["jdoe@example.org"]}}
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the endpoint's success payload parses as a CAS Principal before wiring it in
String body = httpClient.get(restAuthUrl);
JsonNode node = MAPPER.readTree(body);
if (node == null || !node.hasNonNull("id")) {
    throw new IllegalStateException("Response missing principal id");
}

Type guard

boolean isValidPrincipalPayload(String body) {
    try {
        JsonNode n = MAPPER.readTree(body);
        return n != null && n.hasNonNull("id") && n.get("id").isTextual();
    } catch (Exception e) { return false; }
}

Try / catch

try {
    return restHandler.authenticate(credential);
} catch (FailedLoginException e) {
    if (e.getMessage().startsWith("Unable to detect the authentication principal")) {
        LOGGER.error("Principal parse failure — check response shape: {}", e.getMessage());
    }
    throw e;
}

Prevention

When it happens

Trigger: authenticateUsernamePasswordInternal gets status OK, calls buildPrincipalFromResponse, and MAPPER.readValue(result, Principal.class) or principalFactory.createPrincipal throws — e.g. empty body, non-JSON body, JSON lacking '@class'/id, or unexpected attribute types.

Common situations: Endpoint returns an empty or HTML error body with status 200; response JSON does not match the expected Principal structure (missing id, Jackson needs @class typing info); endpoint API changed its response shape after an upgrade; attribute values are of types Jackson cannot map.

Understand the failure class

Related errors


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

Appendix: source

Thrown at support/cas-server-support-rest-authentication/src/main/java/org/apereo/cas/adaptors/rest/RestAuthenticationHandler.java:119

        } finally {
            HttpUtils.close(response);
        }
    }

    protected AuthenticationHandlerExecutionResult buildPrincipalFromResponse(
        final UsernamePasswordCredential credential,
        final HttpResponse response) throws Throwable {
        try {
            try (val content = ((HttpEntityContainer) response).getEntity().getContent()) {
                val result = IOUtils.toString(content, StandardCharsets.UTF_8);
                LOGGER.debug("REST authentication response received: [{}]", result);
                val principalFromRest = MAPPER.readValue(result, Principal.class);
                val principal = principalFactory.createPrincipal(principalFromRest.getId(), principalFromRest.getAttributes());
                return createHandlerResult(credential, principal, getWarnings(response));
            }
        } catch (final Throwable e) {
            LoggingUtils.error(LOGGER, e);
            throw new FailedLoginException("Unable to detect the authentication principal for " + credential.getUsername());
        }
    }

    /**
     * Resolve {@link MessageDescriptor warnings} from the response.
     *
     * @param authenticationResponse The response sent by the REST authentication endpoint
     * @return The warnings for the created {@link AuthenticationHandlerExecutionResult}
     */
    protected List<MessageDescriptor> getWarnings(final HttpResponse authenticationResponse) {
        val messageDescriptors = new ArrayList<MessageDescriptor>(2);

        val passwordExpirationDate = authenticationResponse.getFirstHeader(HEADER_NAME_CAS_PASSWORD_EXPIRATION_DATE);
        if (passwordExpirationDate != null) {
            val days = Duration.between(Instant.now(Clock.systemUTC()), DateTimeUtils.convertToZonedDateTime(passwordExpirationDate.getValue())).toDays();
            messageDescriptors.add(new PasswordExpiringWarningMessageDescriptor(null, days));
        }

View on GitHub (pinned to e7288fc434)