apereo/cas · error · DuoSecurityException
Invalid response format received from Duo
Error message
Invalid response format received from Duo
What it means
DuoSecurityException thrown by BaseDuoSecurityAuthenticationService.getUserAccount() when the JSON returned by the Duo Admin/API endpoint has no "stat" field at all. The service expects every Duo API response to carry "stat" ("OK" or "FAIL"); its absence means the payload is not a recognizable Duo API response. The service cannot tell success from failure, so it treats Duo as unusable and aborts.
Solutions
- Verify the Duo API hostname, integration key (ikey) and secret key (skey) in cas.authn.mfa.duo[0].* configuration — a wrong hostname often yields a foreign JSON body without "stat".
- Enable debug logging (org.apereo.cas.adaptors.duograde to DEBUG) and inspect the logged "Duo response was received in unknown format" payload to identify who actually produced the JSON.
- Check for intermediaries (corporate proxies, API gateways, WAFs) between CAS and Duo that may replace the response body; bypass or allowlist the Duo endpoint.
- Confirm the Duo integration type is supported and the account/API version still returns the classic {stat:...} envelope.
Example fix
// before: response body from wrong endpoint
{"errors":[{"code":"invalid_request"}]}
// after: correct Duo endpoint configured
cas.authn.mfa.duo[0].duo-api-host=api-xxxxxxxx.duosecurity.com
cas.authn.mfa.duo[0].integration-key=DI...
cas.authn.mfa.duo[0].secret-key=... Defensive patterns
Strategy: validation
Validate before calling
// Java: validate Duo response shape before relying on it
JsonNode result = MAPPER.readTree(jsonResponse);
if (result == null || !result.hasNonNull("stat")) {
throw new IllegalArgumentException("Duo response missing 'stat' field: " + jsonResponse);
} Type guard
boolean isDuoApiResponse(JsonNode n) {
return n != null && n.isObject() && n.hasNonNull("stat") && n.get("stat").isTextual();
} Try / catch
try {
account = duoService.getUserAccount(username);
} catch (DuoSecurityException e) {
// mark Duo unavailable, fall back / surface config error
LOGGER.error("Duo unusable: {}", e.getMessage());
} Prevention
- Verify Duo api host, ikey, and skey at deploy time with a /check ping call.
- Inspect raw Duo response bodies in debug logs during initial setup.
- Bypass/allowlist corporate proxies for *.duosecurity.com endpoints.
When it happens
Trigger: Calling getUserAccount (via DuoSecurityAuthenticationService) when the decoded Duo HTTP response body parses as JSON but lacks the "stat" key — e.g. the response is a JSON error object from a proxy/gateway, an HTML error page that happens to parse, or a Duo response from an incompatible API version.
Common situations: Wrong integration key/secret pointing at a non-Duo endpoint; a reverse proxy or SSO gateway intercepting the request and returning its own JSON (e.g. {"error":"unauthorized"}); Duo API hostname misconfigured (typo in api-xxxxxxxx.duosecurity.com); network appliance returning canned JSON error bodies.
Related errors
- Duo returned an Invalid response with message
- Duo returned code
- Unknown Duo Security authentication attempt
- Duo Security passcode authentication has failed
- Duo Security universal prompt authentication has failed
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/6d6043dcfe7769d1.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-duo-core/src/main/java/org/apereo/cas/adaptors/duo/authn/BaseDuoSecurityAuthenticationService.java:95
LOGGER.debug("Found cached duo user account [{}]", account);
return account;
}
val account = new DuoSecurityUserAccount(username);
account.setStatus(DuoSecurityUserAccountStatus.AUTH);
try {
val userRequest = buildHttpPostUserPreAuthRequest(username);
signHttpUserPreAuthRequest(userRequest);
LOGGER.debug("Contacting Duo Security to inquire about username [{}]", username);
val userResponse = getHttpResponse(userRequest);
val jsonResponse = URLDecoder.decode(userResponse, StandardCharsets.UTF_8);
LOGGER.debug("Received Duo response [{}]", jsonResponse);
val result = MAPPER.readTree(jsonResponse);
if (!result.has(RESULT_KEY_STAT)) {
LOGGER.warn("Duo response was received in unknown format: [{}]", jsonResponse);
throw new DuoSecurityException("Invalid response format received from Duo");
}
if ("OK".equalsIgnoreCase(result.get(RESULT_KEY_STAT).asString())) {
val response = result.get(RESULT_KEY_RESPONSE);
val authResult = response.get(RESULT_KEY_RESULT).asString().toUpperCase(Locale.ENGLISH);
val status = DuoSecurityUserAccountStatus.valueOf(authResult);
account.setProviderId(properties.getId());
account.setStatus(status);
account.setMessage(response.get(RESULT_KEY_STATUS_MESSAGE).asString());
if (status == DuoSecurityUserAccountStatus.ENROLL) {
val enrollUrl = response.get(RESULT_KEY_ENROLL_PORTAL_URL).asString();
account.setEnrollPortalUrl(enrollUrl);
}
} else {
val code = result.get(RESULT_KEY_CODE).asInt();
if (code > RESULT_CODE_ERROR_THRESHOLD) {
LOGGER.warn("Duo returned a failure response with code: [{}]. Duo will be considered unavailable",View on GitHub (pinned to e7288fc434)