Security got me down

While investigating the feasability of using JWT for user authentication, I continually hit many security related tangents. I’ll have a post about my JWT findings soon, but for now I’d like to jot down some very simplified explanations of security risks and how they are handled by Rails. I can hopefully do a little mythbustin’ too!

Enter the Dragon

Let us start at jwt.io. What duh nuoc mam is a JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Aite, whole lotta shit going on here. Proceed to break it down, in English pls.

  1. A JWT is JuhSawn object. Dope shit.
  2. It is digitally signed using a secret (HMAC).

Hold ur horses. Via wikipedia:

In cryptography, an HMAC (sometimes expanded as either keyed-hash message authentication code or hash-based message authentication code) is a specific type of message authentication code (MAC) involving a cryptographic hash function and a secret cryptographic key. It may be used to simultaneously verify both the data integrity and the authentication of a message, as with any MAC. Any cryptographic hash function, such as SHA-256 or SHA-3, may be used in the calculation of an HMAC; the resulting MAC algorithm is termed HMAC-X, where X is the hash function used (e.g. HMAC-SHA256 or HMAC-SHA3). The cryptographic strength of the HMAC depends upon the cryptographic strength of the underlying hash function, the size of its hash output, and the size and quality of the key.

There are tons of different hash functions, I was looking at the Ruby implementation of JWT and a variety of SHA hashing algorithms are available: SHA-256, SHA-512-256, SHA-384, and SHA-512. Gonna try a few of these:

[1] pry(main)> require 'jwt'
true
[2] pry(main)> key = "e7224ae91ca00f3dffdee7bf88b14d39b5b6403b9e0221b8532a48e466b5cbe3f7dfb94a318909a3ccfe6009c8e3f685c0f5edfd09181f8b5a86100d68ed2e8e"
"e7224ae91ca00f3dffdee7bf88b14d39b5b6403b9e0221b8532a48e466b5cbe3f7dfb94a318909a3ccfe6009c8e3f685c0f5edfd09181f8b5a86100d68ed2e8e"
[3] pry(main)> payload = { sub: 1, exp: Time.now.to_i + 15 * 60 }
    {
        :sub => 1,
        :exp => 1565926094

    }
[4] pry(main)> tokens = %w(HS256 HS384 HS512).map { |hash_function| JWT.encode(payload, key, hash_function) }
    [
        [0] "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImV4cCI6MTU2NTkyNjA5NH0.UR4dYqhxaKUs87T9XgNTxP2H1k6oP4VVQO1gIT4ybT4",
        [1] "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOjEsImV4cCI6MTU2NTkyNjA5NH0.jXoPtc5DxuJ_R-AJVRdhbhMp1M2qfd1ya087zRs-gc9PtJuZRtecpBShpT1Huv31",
        [2] "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOjEsImV4cCI6MTU2NTkyNjA5NH0.jQMC--gpEzNL1Ea1Pd9XlIBvG9cNvkAmE5W79zh0TMvcJF82I3LCCafVdmM8ZR_PmoAeDdeliDH5KG1jhSRv3g"
    ]
[5] pry(main)> tokens.map(&:size)
    [
        [0] 100,
        [1] 121,
        [2] 143
    ]

There are three parts to a JWT, period separated: header, payload, and the signature. Both the header and payload are then Base64Url encoded (A-Za-Z0-9+/). EDIT JWT uses a variant of Base64 encoding safe for URLs, therefore + and / are replaced with - and _.

The chosen hashing function comes into play for the signature. The signature is generated by taking

base64urlencoded(header) + '.' + base64urlencoded(payload)

and signing it with the key. The result is also Base64Url encoded again. One thing to note here is that so far, the JWT has only been signed, NOT encrypted. That means the token content is readable by everyone. Huh? I misunderstood signing vs encryption.

JWT Signing and Encryption

Signing

Both signing and encryption use cryptography - the study of securely communicating information between parties with the possibility of shitheads that may try to intercept the messages.

There are multiple ways to sign a JWT, either using a secret (HMAC) or using a public/private key pair (RSA/ECDSA). The purpose of signing a JWT is to verify the integrity of the information contained in it. How is this used in authentication? Let’s examing a typical authentication workflow using JWT.

When a client successfully authenticates with the server using their credentials, the server responds with a JWT and refresh token. The JWT specification has a number of required/arbitrary “claims” (information fields). Depending on the authentication scheme, a stateful JWT may contain a session ID that the server can query to obtain the actual session information. A stateless JWT actually has the session information directly contained in its claims.

Assuming the JWT is signed but not encrypted, anyone can read the contents of the JWT. In the case of a stateless JWT, claims such as sub (a user ID) or another claim indicating admin privileges can be exposed. So what if this information was stolen and manipulated by some shithead?

For each authenticated request the client makes to the server, it must attach the JWT to the Authorization HTTP header. The server should check the integrity of the JWT on each request, and this is where signing comes into play. Assuming a secret was used to sign the JWT, the server verifies the integrity by:

  1. Base64Url decode the JWT
  2. Split the JWT into original three period separated parts
  3. Use the secret to sign the first two parts
  4. Check the result signature is equal to the given signature

This is where I started tripping. There was no decryption going on, the decoding of the JWT was simply running the signing process again to generate a new signature and comparing it to the given signature. Assuming some shithead modified the JWT contents, the integrity check would fail since the secret used to sign the JWT is not known. OK.

Next time, I’ll plan on tackling JWT encryption, the role of refresh tokens, and compare/contrast JWTs and sessions and potential security risks involved.