{"record":{"id":"8fc825d4fb576e23","repo":"clockworklabs/SpacetimeDB","slug":"token-sub-claim-is-not-a-string","errorCode":null,"errorMessage":"Token 'sub' claim is not a string","messagePattern":"Token 'sub' claim is not a string","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/bindings/src/lib.rs","lineNumber":1939,"sourceCode":"        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        }\n    }\n","sourceCodeStart":1921,"sourceCodeEnd":1957,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings/src/lib.rs#L1921-L1957","documentation":"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.","triggerScenarios":"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.","commonSituations":"Home-grown identity services using numeric user IDs; mis-serialized fixtures; claims copied from databases where IDs are integers.","solutions":["Issue sub as a string per RFC 7519 (stringify numeric IDs).","Validate token conformance upstream before callers rely on subject().","Fix fixtures so sub is a JSON string."],"exampleFix":"// before: numeric subject panics\nserde_json::json!({\"sub\": 12345});\n\n// after: string subject\nserde_json::json!({\"sub\": \"12345\"});","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fn claims_sub_is_string(claims_json: &str) -> bool {\n    serde_json::from_str::<serde_json::Value>(claims_json)\n        .ok()\n        .and_then(|v| v.get(\"sub\").map(|s| s.is_string()))\n        .unwrap_or(false)\n}","tryCatchPattern":"let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| claims.subject().to_string()));\nif outcome.is_err() {\n    // 'sub' exists but is not a JSON string: fix the issuer to stringify IDs per RFC 7519.\n}","preventionTips":["Always issue sub as a string; stringify numeric user IDs.","Validate issued tokens with a JSON-schema check that pins claim types.","Keep fixtures type-correct: sub: \"12345\", not sub: 12345."],"tags":["jwt","auth","claims","panic","rust"],"backgroundTag":"jwt-invalid-claim-type","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}