Build a JWT in RoR API Using Ruby Only

Munkith Abid
5 min readNov 28, 2021

Leveraging Ruby existing standard libraries to build JWToken

These days, writing programs and apps has become more fast-paced, productivity-oriented than it’s ever been. And the value of task-tailored libraries and frameworks has become undeniably essential and a great companion of any programming language out there. It also plays a significant role in making us programmers like consumers or second-class programmers by adding more layers of abstraction on top of these languages, pushing us further and further from understanding the inner workings and how things function at the language level. For this reason, I find it imperative for the programmer to — every now and then — take a step back and dedicate some time to understand how things are achieved when using some features in these libraries and frameworks.

Today we’re going to do something similar with JWT (Jason Web Token) by creating it from scratch following the same format adopted on the JWT website using Ruby standard libraries and nothing else in a Ruby on Rails API, which in turn follows the JWT Standard as defined in this rfc7519.

Note: This article is meant to deliver the intrinsic parts of Ruby to create and struct JWT exactly as it would be generated using gems like JWT gem. Thus, it will not be involved in creating any wrapper functions or classes to define how the overall structure should look like. This part is entirely left to the reader and I think that RoR is opinionated enough to provide the “rails” to guide you to achieve that.

Let’s start by looking at the end goal of how our JWT should look like, which is something like this:

This string is produced using this formula:

This is great because now we know the recipe to make our JWT; which boils down to this:

We need to have a header part and payload part that are separately encoded using base64 URL encoding, stitch these two together with a dot in between and pass the resulting string to HMACSHA256 function along with a secret key to generate the last part of our token, that is the signature. So now the list of requirements is simple and straightforward:

  • A base64URL encoding function.
  • A hash-based HMACHSA256 function.
  • A secret to sign the payload and header.

Luckily for us, all these ingredients are readily available for use in Ruby and Rails.

To make things simple, I’ll use the example in the JWT debugger as a reference to produce the final token referenced above because it’s available for everyone.

To encode the header and the payload, we can use urlsafe_encode64function of the Base64 module in Ruby std-lib. So for the header, we can do something like this:

[4] pry(main)> header = {"alg": "HS256", "typ": "JWT"}
=> {:alg=>"HS256", :typ=>"JWT"}
[5] pry(main)> json_header = header.to_json
=> "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"
[6] pry(main)> encoded_header = Base64.urlsafe_encode64(json_header)
=> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

And the same thing could be done to the payload:

[12] pry(main)> payload = {"sub":"1234567890","name":"JohnDoe","iat":1516239022}
=> {:sub=>"1234567890", :name=>"JohnDoe", :iat=>1516239022}
[13] pry(main)> json_payload = payload.to_json
=> "{\"sub\":\"1234567890\",\"name\":\"JohnDoe\",\"iat\":1516239022}"
[14] pry(main)> encoded_payload = Base64.urlsafe_encode64(json_payload)
=> "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9"

Great. In very few steps, we were able to generate the first two-thirds of our JWT with an exact match to their counterparts on the JWT website; everything is very straightforward with one little note:

We have to convert our hash to a JSON string using to_json method before passing it over to the encoding function — after all, this is what the ‘J’ stands for in JWT. Also, avoid using as_json because this will return a JSON object/hash instead of a stringified JSON.

Ok, now on to the last part to produce the signature.

Ruby does provide a function to provide a hash-based message generated from the data and a secret key passed to it, but it’s a bit more involved than the previous one, so let's break it down.

The data part is already there, and we just need to make it by attaching the encoded_header and the encoded_payload with a dot in between them.

The secret is also available every time you generate a new Rails API using:

rails new <app_name> --api

Rails will automatically create a file called credentials.yml.enc, which contains application-wide credentials used by different parts of your app, among which a secret key called secret_key_base. It is also encrypted using a master key in a file called a master.key. Both of these files can be found in the config directory, and the credentials.yml file can be opened using this command:

EDITOR="code --wait" rails credentials:edit

Note: The word ‘code’ in the above command references vscode as the editor used to open the file, you can change to your likings.

You can also generate your secret (recommended) to be used only for JWT authentication using this command:

rails secret

and then copy the result to the credentials file, give an appropriate name following the YML syntax and save the changes.

To programmatically retrieve a value saved in the credentials, Rails provide this fetch function:

Rails.application.credentials.fetch(:the_key_to_the_value)

Here is the HMAC function signature that we’re going to use to sign our message:

OpenSSL::HMAC.digest("SHA256", key, data)

All is ready. Here are the last steps along with their respective outputs:

[28] pry(main)> message = encoded_header + '.' + encoded_payload=> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9"[29] pry(main)> secret = '25db74bd9b6dcb56489bf59e8386d617e105af7005b10e7f758a41979ea9c5cef5310f16c17286f2f7bb13efc3e49b16ee7b668a6568348df35f09c6aa77c0aa' 

[30] pry(main)> secret_binary = Base64.decode64(pidkey)
[31] pry(main)> signed_message = OpenSSL::HMAC.digest("SHA256", secret_binary, message)[32] pry(main)> signature = Base64.urlsafe_encode64(signed_message)
=> "E1kx0x18Wf3ZKxSsx2a9YZu0nswD7hVBrozOPosp6SQ="
[33] pry(main)> signature.chomp[0...-1]
=> "E1kx0x18Wf3ZKxSsx2a9YZu0nswD7hVBrozOPosp6SQ"

First, we compose our message that we want to sign, then we initialize our secret variable to whatever value your Rails app has generated, then we have to decode the secret to its binary form before passing it over to HMAC, and finally, we sign our message using the hashing algorithm of our choice — in this case, it’s HSA256 based our JWT website example, And finally we encode the result back using base64 and remove the equal sign from the end of the signature.

Note: your signature will be different mainly because your Rails secret will something different.

Now we can finalize our token by attaching it to our message like so:

[34] pry(main)> token = message + '.' + signature
=> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpv
aG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.
E1kx0x18Wf3ZKxSsx2a9YZu0nswD7hVBrozOPosp6SQ"

That’s it; we can go ahead and push it into our headers response before we send the response back to our frontend if we want:

response.set_header('Authorization', token)

This would make these changes to your API entirely transparent for your frontend if you were using something like device-jwt, as your front end logic relies on the token to be assigned to a header called ‘Authorization’.

--

--