JSON Web Tokens (JWT) with Rails and React

Jeremy Wood
7 min readNov 11, 2021

--

Photo by ZSun Fu on Unsplash

JSON Web Tokens (JWT) are a great way to add safe authentication to your web app. But getting them set up can be frustrating. Here is a simple guide explaining how I got them working in one of my projects.

First off, let’s go over the basic functionality of the site where I’m implementing this. I’m using React Router for routing, and Redux for state management in this project. Any time a user accesses the site, they are directed to the login page. This happens because a user’s data must be stored in the redux store to access any of the site. And a user can only be stored in redux if they have already been authenticated. All other routes are blocked unless a user is signed in. This works because, as a single page web app, the store is cleared with any new request for the site. Any time you make a get request for the web app, or refresh the page, you’re dealing with a completely fresh store with no saved user data inside of it. The user data can only be populated after an authenticated login or signup.

From the login page, a fetch request is fired to determine if the person accessing the site has already logged in. If they have logged in previously from their current browser, they will send a key along with that fetch request that authenticates them automatically. They will then be signed in automatically, and taken to the main page. But we’ll have to come back to that, as a few things need to be in place for that check to work.

We’ll start from when a user logs in. If it’s the first time they’ve logged in on their current browser, or if they logged out of their last session, they will have no automatic login capabilities. They login the old fashion way, where the username and password are authenticated in the Rails portion of our app. For my app I went with the standard bcrypt authenticate method for checking passwords against the database.

Once successfully logged in, I serialize the user data to send back to the frontend, using the 'jsonapi-serializer' gem. This is where we send the user data to the redux store, so the app knows someone has been successfully logged in, and allows the user to view the rest of the app. But along with this serialized user data, I also send a token along for the ride. This token is what’s going to allow the user to automatically log in.

Since we’re passing a user object in to our serializer, we can call any User instance methods on that user object inside of our serializer. And there, in the User model is where the fun really begins.

We’ll need to install the ‘jwt’ gem to get our web token functionality working. This gives us access to the JWT model, which gives us the ability to encode and decode tokens, based on two inputs. When you call JWT.encode(arg1, arg2) for instance, the first input is an object that we use to dynamically change the tokens, and make them different for each user. This should be something unique to the specific user, such as the user’s ID. The second argument is a secret key, that hashes the users unique ID into a seemingly “random” secure string.

Here you can see the get_token method that’s called in our serializer. It takes the ID of the user (self), and creates an id_object. That object is passed to the JWT.encode method, which hashes it with our secret_key, and then returns the hashed token to our serializer. We’ll come back to that secret key in a moment, but let’s continue to follow the token for now

Our user has been authenticated, the serializer now has the user data, and the user’s hashed token, and the data can be displayed in our API for our front end. This takes us back to our login action. Here is my full login action:

Here we see the user’s credentials getting organized and sent out with our fetch request. Assuming we don’t have any status errors, we can now store the user’s token on their browser. This is done with the line:

localStorage.setItem(“token”, jsonResp.data.attributes.token)

Local storage is a web API that allows you to store key-value pairs that persist even when the browser is closed. Much like a cookie, but very easy to setup and use. But like cookies, the data can be retrieved by malicious JavaScript running on websites. Just like cookies, anything stored here must be secured from prying eyes, which is why we’re storing it in hashed JWT format. So what we’re doing here is setting the local storage key of “token” to the value of our hashed web token.

NOTE: THIS IS NOT WHERE YOU SHOULD STORE THE JWT IN A SENSITIVE PRODUCTION ENVIRONMENT. This guide is meant to be a basic example of using a JWT. While storing those tokens in local storage is ok for testing, learning, and personal projects, with truly sensitive information you should further secure your tokens to keep them safe from malicious attacks. You can find a good blog about the dangers, and some other options here.

Now, if you were to refresh the browser, or if you closed the browser and navigated back to the web app, the Redux store would be wiped clean again. But this time you have that token stored in local storage. This time when we hit the login page, the dispatch action we fire off to fetch a logged in user has the correct information, and can automatically log us in. Here’s the basic functionality of the dispatch.

And here’s what that dispatch action looks like.

Here we configure our fetch request object with an extra ‘Authorization’ header containing ‘Bearer ’ plus our token. This takes us to the auto-login route in our backend, and back to our sessions controller.

Here we grab the token from our header, and run it through our other JWT method: decode. We pass the decode method our token, and our app’s secret key to decode the hash. Decode however takes two additional arguments as well. As explained on the ‘jwt’ gem repo:

If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker cannot bypass the algorithm verification step. It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm

The default algorithm is ‘HS256’, so we don’t need to specify it in our encode method. If you’d like a different algorithm however, you can specify it in your encode method.

When we call the decode method with our token, it checks the token against our secret key. If the token is not valid, nil is returned, our auto_login method ends, our auto-login fetch request doesn’t get a response, and nothing happens on our login page. If the decode method passes however, we are returned the ID of the user that we originally hashed. We can then use that ID to find the associated user. Then we just send that user’s serialized data back to the front end, add the user to our Redux store, and the user is allowed access to the site.

To clear the token upon logout, we just run localStorage.setItem(“token”, null) to remove the token from our local storage.

Now the last part of this process is setting the secret key on the backend. In Rails, you have access to a secret “credentials” file, where you can store sensitive information. To access this file, cd into your Rails app’s directory, and on the command line enter:

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

Note that “code” in this case is referring specifically to VSCode editor. If you are using a different IDE, you enter that code editor’s specific prefix. For example, if using Sublime the command would look like:

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

If done correctly, this will open your credentials.yml file, where you can add secret keys for your app to use. These won’t be visible anywhere in the code or be pushed to the repo.

Here you will add a key with the secret code of your choice. You should be using a randomly generated 256-bit key,(a hexadecimal string with 64 characters), and changing it periodically for security. It should look something like this:

secret_jwt_key: 1234567890abc…

You can name the key whatever you’d like. Save the file and close the window and the key will be safely stored away.

Now if you remember, in our back end we had this method in our user model:

Whenever we need the secret key on the back end, we can retrieve it with the Rails.application.credentials method, plus whatever you named your secret key.

And that’s a basic way to implement JWT for authentication in your app. This is definitely a basic, high-level overview of everything involved, and is just meant to get you moving. There are plenty of ways you can improve this system by making it more secure and robust. And as there is no shortage of information out there about authentication, I recommend spending some time familiarizing yourself with the different options and methods out there. It can certainly feel very daunting dealing with authentication and app security at first, but the first step to understanding it is starting small and working your way up.

Resources:

JWT docs

Ruby ‘jwt’ gem

Dangers of local storage - Randall Degges

Personal project repo

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Jeremy Wood
Jeremy Wood

Written by Jeremy Wood

I’m a full stack engineer who loves to learn, solve problems, and fix things! When I’m not working on my code, you’ll usually find me working on my motorcycles.

No responses yet

Write a response