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

  1. Pass only the raw JWT — strip the 'Bearer ' scheme and whitespace before calling validate_token
  2. If your Nevis instance issues opaque/reference tokens, switch to token_validation = "remote" (introspection handles them)
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/8f21cdbd575b5ec7. Report an issue: GitHub.