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
- 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.
- Regenerate/verify the integration key and secret key, and confirm the secret is not truncated or URL-encoded incorrectly in configuration.
- Sync the CAS server clock (NTP) — signature-based Duo requests fail when the Date header is skewed more than 300 seconds.
- 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
- Keep CAS host clocks NTP-synced (Duo signature tolerance is 300s).
- Monitor Duo status.duosecurity.com and set alerting on code > 40000 log warnings.
- Rotate and validate integration/secret keys on a schedule.
- Respect Duo rate limits in batch account lookups.
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
- Invalid response format received from Duo
- Unknown Duo Security authentication attempt
- Duo Security passcode authentication has failed
- Duo Security universal prompt authentication has failed
- Duo Security authentication has failed
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;
}
@OverrideView on GitHub (pinned to e7288fc434)