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

  1. 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".
  2. 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.
  3. Check for intermediaries (corporate proxies, API gateways, WAFs) between CAS and Duo that may replace the response body; bypass or allowlist the Duo endpoint.
  4. 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

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


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)