jsonwebtoken icon indicating copy to clipboard operation
jsonwebtoken copied to clipboard

JSON Error When Decoding JWT with Nested Object in sub Claim

Open richardriman opened this issue 7 months ago • 6 comments

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

  1. Use jsonwebtoken version 9.3.0 (also tested with 8.x).
  2. 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,
    }
    
  3. Encode a token with HS256 (or HS512):
    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();
    
  4. 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_str and serde_json::from_slice works 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 sub claim (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 of jsonwebtoken (8.x).
  • The library uses URL_SAFE_NO_PAD for Base64 decoding (correct) and serde_json::from_slice for 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.

richardriman avatar May 01 '25 16:05 richardriman

I'm running into the same issue after switching a String to Vec<String>. serde_json::decode works but jsonwebtoken::decode does not.

kixelated avatar Aug 12 '25 00:08 kixelated

@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.

kixelated avatar Aug 12 '25 00:08 kixelated

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

mdbetancourt avatar Sep 01 '25 18:09 mdbetancourt

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)

dsykes16 avatar Oct 08 '25 00:10 dsykes16

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)

A more specific error message would be good. It's quite cryptic, and I thought there was an issue with the actual JSON.

kixelated avatar Oct 08 '25 01:10 kixelated

That looks like a raw serde_json error. We need to see if we can provide a better one

Keats avatar Oct 09 '25 07:10 Keats