clockworklabs/SpacetimeDB · critical

Token 'sub' claim is not a string

Error message

Token 'sub' claim is not a string

What it means

Same accessor, next check: the claims contain a sub key but its JSON value is not a string (a number, array, boolean, or object). RFC 7519 requires sub to be a case-sensitive string, so the .expect on as_str() panics for non-conforming tokens.

Source

Thrown at crates/bindings/src/lib.rs:1939

        Self {
            payload: jwt,
            parsed: OnceCell::new(),
            audience: OnceCell::new(),
        }
    }

    fn get_parsed(&self) -> &serde_json::Value {
        self.parsed
            .get_or_init(|| serde_json::from_str(&self.payload).expect("Failed to parse JWT payload"))
    }

    /// Returns the tokens subject, from the sub claim.
    pub fn subject(&self) -> &str {
        self.get_parsed()
            .get("sub")
            .expect("Missing 'sub' claim")
            .as_str()
            .expect("Token 'sub' claim is not a string")
    }

    /// Returns the issuer for these credentials, from the iss claim.
    pub fn issuer(&self) -> &str {
        self.get_parsed().get("iss").unwrap().as_str().unwrap()
    }

    fn extract_audience(&self) -> Vec<String> {
        let Some(aud) = self.get_parsed().get("aud") else {
            return Vec::new();
        };
        match aud {
            serde_json::Value::String(s) => vec![s.clone()],
            serde_json::Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(String::from)).collect(),
            _ => panic!("Unexpected type for 'aud' claim in JWT"),
        }
    }

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Issue sub as a string per RFC 7519 (stringify numeric IDs).
  2. Validate token conformance upstream before callers rely on subject().
  3. Fix fixtures so sub is a JSON string.

Example fix

// before: numeric subject panics
serde_json::json!({"sub": 12345});

// after: string subject
serde_json::json!({"sub": "12345"});
Defensive patterns

Strategy: type-guard

Type guard

fn claims_sub_is_string(claims_json: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(claims_json)
        .ok()
        .and_then(|v| v.get("sub").map(|s| s.is_string()))
        .unwrap_or(false)
}

Try / catch

let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| claims.subject().to_string()));
if outcome.is_err() {
    // 'sub' exists but is not a JSON string: fix the issuer to stringify IDs per RFC 7519.
}

Prevention

When it happens

Trigger: Tokens where sub is numeric (e.g. {"sub": 12345}) issued by a custom auth service; fixtures with an object or boolean in sub; claims converted from another format that kept the native type.

Common situations: Home-grown identity services using numeric user IDs; mis-serialized fixtures; claims copied from databases where IDs are integers.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/8fc825d4fb576e23. Report an issue: GitHub.