apereo/cas · error · DuoSecurityException

Duo returned code

Error message

Duo returned code %s: %s

What it means

DuoSecurityException thrown by BaseDuoSecurityAuthenticationService.getUserAccount() when the Duo API responds with stat != OK and a numeric code greater than RESULT_CODE_ERROR_THRESHOLD (40000). Codes above that threshold indicate a server-side/transport-level failure, so CAS logs a warning and marks Duo as unavailable, propagating the code and message. Lower codes are treated as configuration errors and logged without failing.

Solutions

  1. Read the Duo code and message from the log line "Duo returned a failure response with code" and look it up in Duo's documentation to identify the exact failure.
  2. Regenerate/verify the integration key and secret key, and confirm the secret is not truncated or URL-encoded incorrectly in configuration.
  3. Sync the CAS server clock (NTP) — signature-based Duo requests fail when the Date header is skewed more than 300 seconds.
  4. Check the Duo status page (status.duosecurity.com) and retry after transient outages/rate limiting clear.

Example fix

// before (skewed clock)
Duo returned code 40101: Invalid request — expired

// after: NTP-synced host + verified keys
cas.authn.mfa.duo[0].integration-key=DIXXXXXXXXXXXXXXXXXX
cas.authn.mfa.duo[0].secret-key=(valid skey)
# timedatectl set-ntp true
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: call Duo /check and inspect code
JsonNode check = duoApiCall("/check");
if (check.get("stat").asText().equalsIgnoreCase("FAIL") && check.get("code").asInt() > 40000) {
    throw new IllegalStateException("Duo unavailable, code=" + check.get("code"));
}

Type guard

boolean isDuoServerFailure(JsonNode result) {
    return result.hasNonNull("stat") && "FAIL".equalsIgnoreCase(result.get("stat").asText())
        && result.hasNonNull("code") && result.get("code").asInt() > 40000;
}

Try / catch

try {
    account = duoService.getUserAccount(username);
} catch (DuoSecurityException e) {
    // transient upstream failure: retry with backoff or fail closed
    retryWithBackoff(() -> duoService.getUserAccount(username));
}

Prevention

When it happens

Trigger: Any Duo API call from getUserAccount that returns {"stat":"FAIL","code":>40000,"message":...} — e.g. code 40001 invalid request signature, 40101/40102 auth errors, rate-limit or server-error codes from the Duo Admin API while pre-checking the user account.

Common situations: Expired or wrong integration/secret keys (signature failures); Duo service outage or degraded status; Duo API rate limiting; clock skew on the CAS server breaking the signed request (date header too skewed); integration lacking permission for the admin endpoint.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/45b2229ea9cca57d. 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:115

            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",
                        result.get(RESULT_KEY_MESSAGE));
                    throw new DuoSecurityException("Duo returned code %s: %s".formatted(code, result.get(RESULT_KEY_MESSAGE)));
                }
                LOGGER.warn("Duo returned an Invalid response with message [{}] and detail [{}] "
                        + "when determining user account. This maybe a configuration error in the admin request and Duo will "
                        + "still be considered available.",
                    result.hasNonNull(RESULT_KEY_MESSAGE) ? result.get(RESULT_KEY_MESSAGE).asString() : StringUtils.EMPTY,
                    result.hasNonNull(RESULT_KEY_MESSAGE_DETAIL) ? result.get(RESULT_KEY_MESSAGE_DETAIL).asString() : StringUtils.EMPTY);
            }
        } catch (final Exception e) {
            LOGGER.warn("Reaching Duo has failed with error: [{}]", e.getMessage(), e);
            account.setStatus(DuoSecurityUserAccountStatus.UNAVAILABLE);
        }

        userAccountCachedMap.put(account.getUsername(), account);
        LOGGER.debug("Fetched and cached duo user account [{}]", account);
        return account;
    }

    @Override

View on GitHub (pinned to e7288fc434)