zeroclaw-labs/zeroclaw · error
MFA is required but user '{}' has not completed MFA verifica
Error message
MFA is required but user '{}' has not completed MFA verification What it means
The provider was built with require_mfa = true and the resolved identity has mfa_verified = false. In remote mode mfa_verified is true only when the introspection response carries acr = "mfa" or an amr entry of fido2/passkey/otp/webauthn (nevis.rs:223-228, 146-151). The token itself is valid; the authentication policy is what fails.
Source
Thrown at crates/zeroclaw-runtime/src/security/nevis.rs:147
http_client,
})
}
/// Validate a bearer token and resolve the caller's identity.
/// Returns `NevisIdentity` on success, or an error if the token is invalid,
/// expired, or MFA requirements are not met.
pub async fn validate_token(&self, token: &str) -> Result<NevisIdentity> {
if token.is_empty() {
bail!("empty bearer token");
}
let identity = match self.validation_mode {
TokenValidationMode::Local => self.validate_token_local(token).await?,
TokenValidationMode::Remote => self.validate_token_remote(token).await?,
};
if self.require_mfa && !identity.mfa_verified {
bail!(
"MFA is required but user '{}' has not completed MFA verification",
crate::security::redact(&identity.user_id)
);
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if identity.session_expiry > 0 && identity.session_expiry < now {
bail!("Nevis session expired");
}
Ok(identity)
}
/// Validate token by calling the Nevis introspection endpoint.View on GitHub (pinned to 88bb9c8533)
Solutions
- Have the caller re-authenticate and complete an MFA method (FIDO2, passkey, or OTP), then present the new token
- Inspect the introspection response for the failing token and confirm acr/amr claims are present; fix the IdP client mappers if not
- If MFA is genuinely not required for this deployment, build the provider with require_mfa = false
Example fix
// before: policy enabled let provider = NevisAuthProvider::new(url, realm, client, secret, "remote", None, true, 3600)?; // after: policy matches what the IdP actually enforces let provider = NevisAuthProvider::new(url, realm, client, secret, "remote", None, false, 3600)?;
Defensive patterns
Strategy: try-catch
Try / catch
Match err.to_string().contains("MFA is required") and return 401/403 with an mfa_required signal so the client triggers an MFA re-auth flow; do not retry the same token. Prevention
- Confirm the IdP client emits amr and acr claims before enabling require_mfa
- Return an mfa_required signal instead of a generic 500 so clients can re-authenticate
- Keep a CI test that asserts a password-only token is rejected
When it happens
Trigger: validate_token with a token from a password-only login; the IdP issued the token without amr/acr claims; a custom acr value like "mfa:2" that does not equal "mfa" exactly.
Common situations: SSO flow skips the MFA step for remembered devices; the Nevis client mapper omits the amr claim; require_mfa was enabled after long-lived tokens were already issued.
Related errors
- empty bearer token
- Nevis session expired
- Nevis introspection returned HTTP {}
- Token is not active (revoked or expired)
- Invalid JWT structure: expected 3 dot-separated parts
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/8661d11ce5e7c572.
Report an issue: GitHub.