zeroclaw-labs/zeroclaw · error
Nevis session expired
Error message
Nevis session expired
What it means
The token resolved to an identity, but identity.session_expiry (the exp claim from introspection, nevis.rs:229) is earlier than the local clock (nevis.rs:158-160). A session_expiry of 0 (missing exp) skips the check, so this fires only when a concrete past expiry was reported.
Source
Thrown at crates/zeroclaw-runtime/src/security/nevis.rs:159
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.
async fn validate_token_remote(&self, token: &str) -> Result<NevisIdentity> {
let introspect_url = format!(
"{}/auth/realms/{}/protocol/openid-connect/token/introspect",
self.instance_url.trim_end_matches('/'),
self.realm,
);
let mut form = vec![("token", token), ("client_id", &self.client_id)];
// client_secret is optional (public clients don't need it)
let secret_ref;
if let Some(ref secret) = self.client_secret {
secret_ref = secret.as_str();View on GitHub (pinned to 88bb9c8533)
Solutions
- Send the caller through re-authentication or token refresh, then retry with the new token
- Verify host clock sync (NTP/systemd-timesyncd) on the machine running ZeroClaw
- If you mint test tokens, confirm exp is epoch seconds, not milliseconds, and in the future
Example fix
// before
match provider.validate_token(token).await {
Ok(id) => id,
Err(e) => return internal_error(e),
}
// after
match provider.validate_token(token).await {
Ok(id) => id,
Err(e) if e.to_string().contains("Nevis session expired") => return unauthorized_reauth(),
Err(e) => return internal_error(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
fn jwt_expired(token: &str) -> Option<bool> {
let payload = token.split('.').nth(1)?;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload).ok()?;
let claims: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
let exp = claims.get("exp")?.as_u64()?;
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
Some(exp < now)
} Try / catch
Match err.to_string().contains("Nevis session expired") and return 401 with a re-authenticate challenge; treat it as a user-state event, not a server error. Prevention
- Keep host clocks NTP-synced so skew never manufactures false expiries
- Refresh tokens ahead of expected expiry instead of validating until failure
- Never hardcode past exp values in test fixtures
When it happens
Trigger: validate_token with a token whose exp is in the past; local host clock running ahead of the IdP (NTP drift); a stale token fixture reused in tests.
Common situations: Resuming an integration test with an old token; VM or container clock skew after sleep or migration; cached token reused after the user logged out elsewhere.
Related errors
- empty bearer token
- MFA is required but user '{}' has not completed MFA verifica
- 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/6c5b5400da8e6911.
Report an issue: GitHub.