zeroclaw-labs/zeroclaw · error
Invalid JWT structure: expected 3 dot-separated parts
Error message
Invalid JWT structure: expected 3 dot-separated parts
What it means
In local mode validate_token_local splits the token on '.' and requires exactly 3 parts (nevis.rs:236-239). Any string that is not header.payload.signature fails here — before the not-implemented bail. This is a shape sanity check only; no signature work happens.
Source
Thrown at crates/zeroclaw-runtime/src/security/nevis.rs:238
.split_whitespace()
.map(String::from)
.collect(),
mfa_verified: body.acr.as_deref() == Some("mfa")
|| body
.amr
.iter()
.flatten()
.any(|m| m == "fido2" || m == "passkey" || m == "otp" || m == "webauthn"),
session_expiry: body.exp.unwrap_or(0),
})
}
#[allow(clippy::unused_async)] // Will use async when JWKS validation is implemented
async fn validate_token_local(&self, token: &str) -> Result<NevisIdentity> {
// JWT structure check: header.payload.signature
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
bail!("Invalid JWT structure: expected 3 dot-separated parts");
}
bail!(
"Local JWKS token validation is not yet implemented. \
Set token_validation = \"remote\" to use the Nevis introspection endpoint."
);
}
/// Validate a Nevis session token (cookie-based sessions).
pub async fn validate_session(&self, session_token: &str) -> Result<NevisIdentity> {
if session_token.is_empty() {
bail!("empty session token");
}
let session_url = format!(
"{}/auth/realms/{}/protocol/openid-connect/userinfo",
self.instance_url.trim_end_matches('/'),
self.realm,View on GitHub (pinned to 88bb9c8533)
Solutions
- Pass only the raw JWT — strip the 'Bearer ' scheme and whitespace before calling validate_token
- If your Nevis instance issues opaque/reference tokens, switch to token_validation = "remote" (introspection handles them)
- Pre-check token.split('.').count() == 3 in your middleware to fail with a clearer 401
Example fix
// before
let token = authorization_header; // "Bearer eyJhbGci..."
let id = provider.validate_token(&token).await?;
// after
let token = authorization_header
.strip_prefix("Bearer ")
.unwrap_or(&authorization_header)
.trim();
let id = provider.validate_token(token).await?; Defensive patterns
Strategy: validation
Validate before calling
fn is_jwt_shaped(token: &str) -> bool {
let parts: Vec<&str> = token.split('.').collect();
parts.len() == 3 && parts.iter().all(|p| !p.is_empty())
}
if !is_jwt_shaped(token) { return unauthorized("malformed token"); } Try / catch
Match err.to_string().contains("Invalid JWT structure") and return 401 with a generic 'malformed token' message — log only the part count, never the token. Prevention
- Strip the Bearer scheme in exactly one place before token use
- Know whether your IdP issues JWTs or opaque reference tokens before choosing local mode
- Log token shape (part count) instead of token content when debugging
When it happens
Trigger: validate_token with token_validation = "local" and a non-JWT value: an opaque access token, an API key, or the full 'Bearer eyJ...' header with the scheme prefix still attached.
Common situations: Middleware forwards the whole Authorization header value instead of the token; the IdP issues opaque reference tokens that can never satisfy local JWT validation; a truncated paste loses a segment.
Related errors
- empty bearer token
- empty session token
- MFA is required but user '{}' has not completed MFA verifica
- Nevis session expired
- Nevis introspection returned HTTP {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/8f21cdbd575b5ec7.
Report an issue: GitHub.