clockworklabs/SpacetimeDB · critical

Missing 'sub' claim

Error message

Missing 'sub' claim

What it means

JwtClaims::subject() reads the sub claim of the parsed JWT claims; this .expect panics when the claims JSON is valid but contains no sub key. Standard identity JWTs always carry sub, so its absence means a malformed, custom, or test-fixture token.

Source

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

impl JwtClaims {
    fn new(jwt: String) -> Self {
        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. Ensure tokens issued by your identity provider always include a sub claim.
  2. Guard access to subject() - check for the claim's presence before calling when you control the claims source.
  3. Fix test fixtures to include sub.

Example fix

// before: fixture without a subject panics when subject() is called
AuthCtx::from_jwt_payload(serde_json::json!({"iss": "https://ex"}).to_string());

// after: include a string 'sub'
AuthCtx::from_jwt_payload(serde_json::json!({"sub": "user-1", "iss": "https://ex"}).to_string());
Defensive patterns

Strategy: type-guard

Type guard

// You cannot inspect private fields, but if you control the claims source,
// validate before issuing/forwarding the token payload:
fn claims_have_string_sub(claims_json: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(claims_json)
        .ok()
        .and_then(|v| v.get("sub").and_then(|s| s.as_str()).map(|_| true))
        .unwrap_or(false)
}

Try / catch

let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| claims.subject().to_string()));
if outcome.is_err() {
    // Token had no 'sub': reject this token at your auth boundary and require
    // identity providers to issue subject claims.
}

Prevention

When it happens

Trigger: Calling ctx.jwt().unwrap().subject() when the connected client presented a token without a subject claim; test fixtures built with from_jwt_payload that omit sub; service/API-key style tokens issued without sub.

Common situations: Custom or in-house auth flows issuing subject-less tokens; test fixtures trimmed down too far; claims mapped from another identity format that drops sub.

Related errors


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