{"record":{"id":"b1f4af81efc0b766","repo":"clockworklabs/SpacetimeDB","slug":"missing-sub-claim","errorCode":null,"errorMessage":"Missing 'sub' claim","messagePattern":"Missing 'sub' claim","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/bindings/src/lib.rs","lineNumber":1937,"sourceCode":"impl JwtClaims {\n    fn new(jwt: String) -> Self {\n        Self {\n            payload: jwt,\n            parsed: OnceCell::new(),\n            audience: OnceCell::new(),\n        }\n    }\n\n    fn get_parsed(&self) -> &serde_json::Value {\n        self.parsed\n            .get_or_init(|| serde_json::from_str(&self.payload).expect(\"Failed to parse JWT payload\"))\n    }\n\n    /// Returns the tokens subject, from the sub claim.\n    pub fn subject(&self) -> &str {\n        self.get_parsed()\n            .get(\"sub\")\n            .expect(\"Missing 'sub' claim\")\n            .as_str()\n            .expect(\"Token 'sub' claim is not a string\")\n    }\n\n    /// Returns the issuer for these credentials, from the iss claim.\n    pub fn issuer(&self) -> &str {\n        self.get_parsed().get(\"iss\").unwrap().as_str().unwrap()\n    }\n\n    fn extract_audience(&self) -> Vec<String> {\n        let Some(aud) = self.get_parsed().get(\"aud\") else {\n            return Vec::new();\n        };\n        match aud {\n            serde_json::Value::String(s) => vec![s.clone()],\n            serde_json::Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(String::from)).collect(),\n            _ => panic!(\"Unexpected type for 'aud' claim in JWT\"),\n        }","sourceCodeStart":1919,"sourceCodeEnd":1955,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings/src/lib.rs#L1919-L1955","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: fixture without a subject panics when subject() is called\nAuthCtx::from_jwt_payload(serde_json::json!({\"iss\": \"https://ex\"}).to_string());\n\n// after: include a string 'sub'\nAuthCtx::from_jwt_payload(serde_json::json!({\"sub\": \"user-1\", \"iss\": \"https://ex\"}).to_string());","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// You cannot inspect private fields, but if you control the claims source,\n// validate before issuing/forwarding the token payload:\nfn claims_have_string_sub(claims_json: &str) -> bool {\n    serde_json::from_str::<serde_json::Value>(claims_json)\n        .ok()\n        .and_then(|v| v.get(\"sub\").and_then(|s| s.as_str()).map(|_| true))\n        .unwrap_or(false)\n}","tryCatchPattern":"let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| claims.subject().to_string()));\nif outcome.is_err() {\n    // Token had no 'sub': reject this token at your auth boundary and require\n    // identity providers to issue subject claims.\n}","preventionTips":["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."],"tags":["jwt","auth","claims","panic","rust"],"backgroundTag":"jwt-missing-claim","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}