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
- Ensure tokens issued by your identity provider always include a sub claim.
- Guard access to subject() - check for the claim's presence before calling when you control the claims source.
- 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
- Require a sub claim in tokens accepted by your auth flow.
- Include sub in every test fixture used with from_jwt_payload.
- Prefer reading claims you control rather than assuming their shape.
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
- Token 'sub' claim is not a string
- Failed to parse JWT payload
- Issuer too long: {:?}
- Subject too long: {:?}
- Issuer empty
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/b1f4af81efc0b766.
Report an issue: GitHub.