Why is a JWT split into three dot-delimited parts?

Viewed 3037

A JSON Web Token (JWT) is split into three Base-64-encoded parts, which are concatenated by periods ("."). The first two parts encode JSON objects, the first of which is a header detailing the signature and hashing algorithm, and the second contains the assertions. The third is binary data that is the signature itself.

My question is: why is the JSON Web Token split into three separate parts like this? It seems like it would have made parsing them a lot easier to have encoded them as a single JSON object, like so (the example below is incomplete for brevity's sake):

{
    "header": {
        "alg": "rsa"
    },
    "assertions": {
        "iss": "2019-10-09T12:34:56Z"
    },
    "sig": "qoewrhgoqiethgio3n5h325ijh3=="
}

Stated differently: why didn't the designers of JWT just put all parts of the JWT in a single JSON object like shown above?

3 Answers

IMHO, it would bring cause more issues. Yes you could parse it nicely, but what about verification of signature?

The Structure of a JWT is <B64 String>.<B64 String>.<B64 String>. Signature is basically the 2 first parts signed. It is unlikely that the structure will be modified by various frameworks in any way.

Now consider JSON: during serialisation and deserialisation the order of elements may chang. Object {"a":1,"b":2} and {"b":2,"a":1} might be equal in javascript but if you stringify them, they will generate different signatures.

Also, to check the signature you would need to decide upon a standard form of JSON that will be used to generate signature (for instance, beautified or minified). Again, different choices will generate different signatures.

As a result there are more hassles than benefits from simply using JSON

While I'm not speaking for the people who designed the JWT, I can think of one major reason why your suggestion won't fly:

Headers don't allow newlines

Remember that a primary use-case for a JWT is to use it as a cookie value. Cookies get passed up in headers. Header values don't support newlines: each header key/value pair needs to fit on one line.

Therefore, arbitrary JSON will not work for something that is meant to be passed as a header value in an HTTP request.

Therefore, some sort of encoding is required - which is why base64 is used in the first place. The reason base64 often shows up is because it converts any blob or string into something that can be reliably transported as simple ascii in almost any circumstances. I.e. three base64 encoded "payloads" separated with periods (which isn't a valid character in base64 encoding) is pretty much guaranteed to transport safely and without mangling between just about any system.

JSON cannot make the same guarantees. Of course, you could remove the newlines (JSON ignores whitespace anyway), but quotes are still a problem: they should be encoded in URLs, and maybe not in other circumstances, although they would probably be okay in an HTTP headers. As a result it just becomes one more "gotcha" as a developer tries to implement it for the first time.

I'm sure there are other reasons too: this isn't meant to be a comprehensive list.

The signature can not be a part of what is signed, therefore it has to be separate.

The header and payloads could be combined into on JSON object, but it would be bad design. It is the job of your JWT library to check the headers and verify the signature. That can (and should) be done without concern for the payload. It is the job for your application to react to the payload. As long as the signature checks out, that can be done without concern for the headers.

Separate conserns, separate objects.

Related