Tech: React JWT Authentication

Eric C Smith
3 min readFeb 25, 2021

--

If you know what this is, you’re probably not a software engineer.

In my previous life as Network Engineer, I thought about things like routing, switching and AAA (Authentication, Authorization and Accounting) a *lot*. Like pretty much all day, every day. So it was kind of nice to see that there is some notion of these things in software as well. The functions are pretty much the same, except one happens on LANs and WANs, and the other happens inside a program’s functionality. The React library has a “React Router” that routes the user to different URLs, that utilizes a “Switch” that matches a route with some functionality(like rendering a clickable link), and what the software guys colloquially refer to as “auth” can be done using JSON Web Tokens, or JWT.

There are several steps to implementing a fully operational JWT, this post will focus on the login process from a user’s perspective, and what happens in the code in order to give him access to protected content. The control flow looks like this:

The user, “Bob” already has a valid account, so when the website prompts him for his credentials he enters them. The client app then passes them to an API on the backend that tries to find them in a database containing all the current users and their credentials. If it finds a match the API generates a token and returns a copy to Bob’s browser. As he browses the protected content, every time Bob makes a request to the site the app checks the token stored in the browser against it’s own copy, thus “authenticating” the user.

The implementation of JWT starts at the backend (Rails server) with two functions to encode and decode the “payload” (the users credential data), JWT.encode and JWT.decode:

class ApplicationController < ActionController::API
def encode_token(payload)
# payload => { username: 'password' }
JWT.encode(payload, 'my_s3cr3t')
# jwt string: "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"
end

def decoded_token(token)
# token => "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"

JWT.decode(token, 'my_s3cr3t')[0]
# JWT.decode => [{ "username"=>"password" }, { "alg"=>"HS256" }]
# [0] gives us the payload { "username"=>"password" }
end
end

The JWT.encode function takes 3 arguments: a payload to encode, an application secret of the user’s choice, and an optional third that can be used to specify the hashing algorithm used. Typically, we don’t need to show the third. This method returns a JWT as a string. JWT.decode takes three arguments as well: a JWT as a string, an application secret, and — optionally — a hashing algorithm. Here we’ve added the functionality at the Application Controller level because all of the pages of my app are protected content. The browser passes the token to the app via a “fetch” request that uses an Authorization header:

fetch('http://localhost:3000/api/v1/profile', {
method: 'GET',
headers: {
Authorization: `Bearer <token>`
}
})

Because we know the format of the data the server will be receiving, we can define a function that handles that data:

def auth_header
# { 'Authorization': 'Bearer <token>' }
request.headers['Authorization']
end

def decoded_token
if auth_header
token = auth_header.split(' ')[1]
# headers: { 'Authorization': 'Bearer <token>' }
JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
# JWT.decode => [{ "username"=>"password" }, { "alg"=>"HS256" }]
end
end

There’s still a lot more, including error handling, user log-in, locking out unauthorized users, allowing sign-up and more. In a later blog post I’ll finish up the JWT access flow, and talk about how to let new users complete a process to be granted access.

--

--