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
- Inspect the actual response body (curl the endpoint) and confirm it is valid JSON with an id and attributes matching CAS Principal format
- Add the Jackson @class type info or align the endpoint payload with the expected Principal shape
- Check CAS logs above this message for the underlying deserialization exception to pinpoint the field
- 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
- Contract-test the endpoint's 200 payload against CAS Principal deserialization
- Include Jackson @class typing info or match the expected Principal JSON shape
- Log the raw response body (safely) when deserialization fails
- Pin endpoint API versions so response shapes do not shift unexpectedly
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Unable to accept response status
- No credentials can be extracted to authenticate the REST…
- Unable to extract credentials for multifactor authentication
- Could not authenticate forbidden account for
- Could not authenticate account for
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)