What Is JWT and Why Does It Matter
As web and mobile applications integrate with other services, it becomes increasingly important to be able to give access in a secure fashion. Introduced in RFC 7519 in May 2015, JSON Web Tokens (JWT) has quickly become a trusted way to solve this problem.
Before we get to JWT, let’s think about security. You can think of most security schemes in three ways: what you know (password), what you have (smart card) or what you are (fingerprint). What happens though when I want to give someone access to a system, but I don’t want to give my password or give them a smart card to get in at any time? We’ll come back to this.
So what is JWT?
At a base level, you can think of JWT as having three parts.
Header
The header states the algorithm used to encrypt the signature and the type, JWT.
header = {alg: "HS256", typ: "JWT"} => {:alg=>"HS256", :typ=>"JWT"}
Payload
The payload is dictated by the application who will receive it. An example payload could be a user ID and whether or not the user is an admin. Unless you use JSON Web Encryption, any data stored in the payload should be considered insecure. In addition, there are special fields that add expiry time and “not before” timestamps to limit when tokens are considered valid.
payload = {user_id: 1, admin: true, exp: 1480532324} => {:user_id=>1, :admin=>true, :exp=>1480532324}
Signature
The signature is generated by hashing a secret string key with a combination of the header and payload. Both the header and payload must be Base64 urlsafe encoded.
encoded_header = Base64.urlsafe_encode64(header.to_json.encode("UTF-8")).delete("=") => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
encoded_payload = Base64.urlsafe_encode64(payload.to_json.encode("UTF-8")).delete("=") => "eyJ1c2VyX2lkIjoxLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxNDgwNTMyMzI0fQ"
Once we have the unsigned token, we can generate the signature. We do this by hashing the secret key with the combination of the encoded header and encoded payload.
key = 'secret' => "secret" unsigned_token = encoded_header + "." + encoded_payload => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxNDgwNTMyMzI0fQ" signature = OpenSSL::HMAC.digest("sha256", key, unsigned_token) => "E&\xEAZ/1\xC1'\x02H\xE6wQ\x06\x9B\x10\x14\x87$\xB0^\xF0\xFB\xA6\xCD\x92\xCFH\xAE\x85\xA1\xC2" encoded_signature = Base64.urlsafe_encode64(signature).delete("=") => "VkD2JXzAP-hgpKbVX14gII8faO3NadRGyEWelQcg5Yo"
You might wonder why we’ve been deleting ‘=’ characters. When you base64 encode a string, one or two ‘=’ signs might be added to pad the string out to the proper length. For JWT, these need to be removed as the signature will not be valid otherwise.
Making a JWT is as simple as joining the unsigned token with the signature with a period. For the above header and payload, we end up with the following token.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJhZG1pbiI6dHJ1ZSwiZXhwIjoxNDgwNTMyMzI0fQ.VkD2JXzAP-hgpKbVX14gII8faO3NadRGyEWelQcg5Yo
Although the below demonstrates the process of creating a JWT in Ruby, JWT has been implemented in most languages
So What’s the Big Deal?
Let’s think about this concretely. Twilio just recently launched a video service where users can join a room to meet. Let’s say we launch the next great dating application, Flair. When people match, they can quickly connect over video conference on their mobile or desktop browser. We all know the world needs another dating app, but pictures just don’t cut it anymore. The thing is we need to make sure that only the potential couple can enter the conference room.
In short, we need a secure, short-term way to give access.
JWT solves this problem. The payload in this case might contain information about our Twilio API key and application ID. In fact, this is exactly the technique that Twilio uses for security. With an added expiry time, Flair can ensure that our lovebirds will have the privacy to talk about long walks on the beach, their favorite color, and make jokes about all the flair they used to wear at Chotchkie’s.
Wait — how does it do that?
When attempts are made to connect to Twilio, our token is decrypted based on the header (which indicates which algorithm was used) and is validated against the secret string known only to our application and Twilio. If the payload was tampered with at any point, the signature would not match. The consuming application would consider the request invalid.
Now that’s what I call privacy.