JSON Error When Decoding JWT with Nested Object in sub Claim
JSON Error When Decoding JWT with Nested Object in sub Claim
When decoding a JWT with a nested object in the sub claim, jsonwebtoken fails with the error Error::Json("expected ',' or '}'", line: 1, column: 8), despite the JSON payload being valid and manually deserializable.
Steps to Reproduce
- Use
jsonwebtokenversion 9.3.0 (also tested with 8.x). - Define a claim struct with a nested object in
sub:use serde::{Serialize, Deserialize}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SubClaims { user_id: i32, tenant_name: String, tenant_id: i32, } #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: SubClaims, exp: usize, iss: String, aud: String, } - Encode a token with
HS256(orHS512):use jsonwebtoken::{encode, Header, Algorithm, EncodingKey, Validation}; let claims = Claims { sub: SubClaims { user_id: 105, tenant_name: "test".to_string(), tenant_id: 1, }, exp: 10000000000, iss: "Issuer".to_string(), aud: "Audience".to_string(), }; let secret = "SOME SECRET"; let token = encode( &Header::new(Algorithm::HS512), &claims, &EncodingKey::from_secret(secret.as_ref()), ).unwrap(); - Attempt to decode:
use jsonwebtoken::{decode, DecodingKey}; let mut validation = Validation::new(Algorithm::HS512); validation.set_audience(&["Audience"]); validation.set_issuer(&["Issuer"]); let claims = decode::<Claims>( &token, &DecodingKey::from_secret(secret.as_ref()), &validation, );
Expected Behavior
The token should decode successfully into the Claims struct.
Actual Behavior
Decoding fails with Error::Json("expected ',' or '}'", line: 1, column: 8).
Additional Notes
- Manual deserialization of the payload using
serde_json::from_strandserde_json::from_sliceworks correctly:use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; let parts: Vec<&str> = token.split('.').collect(); let payload = parts[1]; let decoded = URL_SAFE_NO_PAD.decode(payload).unwrap(); let payload_str = String::from_utf8(decoded).unwrap(); let claims = serde_json::from_str::<Claims>(&payload_str).unwrap(); // Works let claims = serde_json::from_slice::<Claims>(&decoded).unwrap(); // Works - Decoding works with a simple
subclaim (e.g.,sub: String):#[derive(Debug, Serialize, Deserialize)] struct SimpleClaims { sub: String, exp: usize, iss: String, aud: String, } - The issue persists with
HS256,HS512, and older versions ofjsonwebtoken(8.x). - The library uses
URL_SAFE_NO_PADfor Base64 decoding (correct) andserde_json::from_slicefor deserialization. - The raw bytes of the payload are valid UTF-8 and match the JSON:
Payload: {"sub":{"userId":105,"tenantName":"info","tenantId":1},"exp":10000000000,"iss":"Issuer","aud":"Audience"}
Environment
- Rust version: 1.82.0
- jsonwebtoken: 9.3.0 (also tested with 8.x)
- serde: 1.0.210
- serde_json: 1.0.128
- base64: 0.22
Possible Cause
The issue likely stems from jsonwebtoken passing an incorrect or truncated byte slice to serde_json::from_slice when deserializing a nested object in sub, or from a deserialization incompatibility with complex structures.
Request
Please investigate why jsonwebtoken::decode fails with nested objects in sub and provide a fix or clarification on whether complex sub claims are supported.
I'm running into the same issue after switching a String to Vec<String>. serde_json::decode works but jsonwebtoken::decode does not.
@richardriman jk figured out the issue. sub is a reserved field in JWT and using anything other than a string is bad. Rename your SubClaims field and it'll work.
i took i while to find the error the problem is jsonwebtoken deserialize twice the structure one for the custom claim (which deserialize just fine) and another for
#[derive(Deserialize)]
pub(crate) struct ClaimsForValidation<'a> {
#[serde(deserialize_with = "numeric_type", default)]
exp: TryParse<u64>,
#[serde(deserialize_with = "numeric_type", default)]
nbf: TryParse<u64>,
#[serde(borrow)]
sub: TryParse<Cow<'a, str>>,
#[serde(borrow)]
iss: TryParse<Issuer<'a>>,
#[serde(borrow)]
aud: TryParse<Audience<'a>>,
}
#[derive(Debug)]
enum TryParse<T> {
Parsed(T),
FailedToParse,
NotPresent,
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for TryParse<T> {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
Ok(match Option::<T>::deserialize(deserializer) {
Ok(Some(value)) => TryParse::Parsed(value),
Ok(None) => TryParse::NotPresent,
Err(_) => TryParse::FailedToParse,
})
}
}
this is where the error come from (notice sub as a string) still i think this library should provide a better DX pointing out what the error is
Per RFC 7519:
The "sub" value is a case-sensitive string containing a StringOrURI value
The current implementation in jsonwebtoken appears to be compliant with that RFC. This request is for non-RFC-compliant behavior. Is there a non-custom use case for this? (e.g. some major cloud provider or service that issues non-RFC-compliant JWTs)
Per RFC 7519:
The "sub" value is a case-sensitive string containing a StringOrURI value
The current implementation in
jsonwebtokenappears to be compliant with that RFC. This request is for non-RFC-compliant behavior. Is there a non-custom use case for this? (e.g. some major cloud provider or service that issues non-RFC-compliant JWTs)
A more specific error message would be good. It's quite cryptic, and I thought there was an issue with the actual JSON.
That looks like a raw serde_json error. We need to see if we can provide a better one