{"record":{"id":"6c5b5400da8e6911","repo":"zeroclaw-labs/zeroclaw","slug":"nevis-session-expired","errorCode":null,"errorMessage":"Nevis session expired","messagePattern":"Nevis session expired","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/nevis.rs","lineNumber":159,"sourceCode":"        let identity = match self.validation_mode {\n            TokenValidationMode::Local => self.validate_token_local(token).await?,\n            TokenValidationMode::Remote => self.validate_token_remote(token).await?,\n        };\n\n        if self.require_mfa && !identity.mfa_verified {\n            bail!(\n                \"MFA is required but user '{}' has not completed MFA verification\",\n                crate::security::redact(&identity.user_id)\n            );\n        }\n\n        let now = std::time::SystemTime::now()\n            .duration_since(std::time::UNIX_EPOCH)\n            .unwrap_or_default()\n            .as_secs();\n\n        if identity.session_expiry > 0 && identity.session_expiry < now {\n            bail!(\"Nevis session expired\");\n        }\n\n        Ok(identity)\n    }\n\n    /// Validate token by calling the Nevis introspection endpoint.\n    async fn validate_token_remote(&self, token: &str) -> Result<NevisIdentity> {\n        let introspect_url = format!(\n            \"{}/auth/realms/{}/protocol/openid-connect/token/introspect\",\n            self.instance_url.trim_end_matches('/'),\n            self.realm,\n        );\n\n        let mut form = vec![(\"token\", token), (\"client_id\", &self.client_id)];\n        // client_secret is optional (public clients don't need it)\n        let secret_ref;\n        if let Some(ref secret) = self.client_secret {\n            secret_ref = secret.as_str();","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/nevis.rs#L141-L177","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nmatch provider.validate_token(token).await {\n    Ok(id) => id,\n    Err(e) => return internal_error(e),\n}\n\n// after\nmatch provider.validate_token(token).await {\n    Ok(id) => id,\n    Err(e) if e.to_string().contains(\"Nevis session expired\") => return unauthorized_reauth(),\n    Err(e) => return internal_error(e),\n}","handlingStrategy":"try-catch","validationCode":"fn jwt_expired(token: &str) -> Option<bool> {\n    let payload = token.split('.').nth(1)?;\n    let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload).ok()?;\n    let claims: serde_json::Value = serde_json::from_slice(&bytes).ok()?;\n    let exp = claims.get(\"exp\")?.as_u64()?;\n    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();\n    Some(exp < now)\n}","typeGuard":null,"tryCatchPattern":"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.","preventionTips":["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"],"tags":["auth","nevis","session-expiry","clock-skew","rust"],"backgroundTag":"session-expired","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}