How to use JWT in Next.js and Rails

Photo by Mathieu Stern on Unsplash

Next.js has several methods to provide authentication. Being that it is one of the main players in the JAMstack game, it can do this without a traditional backend as well. But if are coming with your own backend, be it in Express, Django, or in this example Rails, Next.js can handle that too. JSON Web Tokens are a viable and secure option.

What is JSON Web Tokens?

A JSON Web Token is an encoded Token that can be sent over the Web with JSON. More specifically it is an encoded string that contains data that can be decoded to reveal this data. For example, when you login to a website and your credentials are verified on the server to be true, an JSON Web Token is sent back to the client with your first name. This enables the client to greet you with a pleasant welcome message.

Create Controlled Login

Let’s create a simple login with a controlled form in Next.js. First off we will start off with just the form. We will be using just an email and a password.

<form><input type='text' placeholder='Enter email'  /><input type='password' placeholder='Enter password'  /><input type='submit' /></form>
A simple email and password login form

What do we do if we want this to be a controlled form in Next.js? We import the useState hook from the react library.

import React, { useState } from 'react';

The state we are trying to keep track of is the email and password. So we need to set a variable name and have a method for changing the value of the variable as the user types. Let’s put the useState hook to action as well as destructuring assignment.

const [email, setEmail] = useState('')const [password, setPassword] = useState('')

Now we must add an onChange Listener that will update the state for each input as the user types.

<form><input type='text' placeholder='Enter email' onChange={ e => setEmail(e.target.value)} /><input type='password' placeholder='Enter password' onChange={ e => setPassword(e.target.value)} /><input type='submit' /></form>

Submit Login Credentials

Now that we have state updating as the user types, we want to handle the form submission and direct it to the backend. Let’s handle the submission first by adding a handle submit function to our form tag. Notice we also pass in the email and password state variables.

<form onSubmit={ e => handleSubmit(e, email, password) } />

Make sure to prevent the form from submitting the traditional way with e.preventDefault(). You can use whatever method you like to submit the post request. I am using Axios and structuring the email and password inside a user object.

const handleSubmit = (e, email, password) => {  e.preventDefault()  axios.post('http://127.0.0.1:4000/login', {user: {email: email, password: password}}, {withCredentials: true})  .then( response => {    if ( response.data.errors ) {      setError(response.data.errors)    }    else {      setError("")      localStorage.setItem("token", response.data.jwt)    }  })}

We will return to this form in a bit. Now let us set up our Rails action and JSON Web Tokens.

Enter JSON Web Tokens…

Let’s assume you already have a database, users, and now session controller set up. You know are able to receive requests from the client, but you are just wondering how to implement JSON Web Tokens. The first thing to do is to install the jwt gem. Add this line to your Gemfile.

gem 'jwt'

Then install from the command line.

bundle install

In this login action inside the Sessions Controller if we are able to find a worker and authenticate the accompanying password we will create a JSON Web Token. To create the token we use the encode method on JWT.

Payload is a hash where we can include any information we want to be available inside the token. The token will be store in the browser localStorage. Remember that the token will be a means of authenticating users and a way of protecting routes within your application from unauthorized users. This token can easily be decoded and the information inside will be revealed. Never would we want to put a password inside these tokens. A user id will usually be enough.

The second argument to encode is the ‘secret’ needed to decode the token and reveal the data. Create a good ‘secret’ and store it inside a .env.

To summarize: If we can verify the credentials we send a token. If not, we send an error message.

def login  @worker = Worker.find_by(email: signup_params[:email])  if @worker && @worker.authenticate(signup_params[:password])    payload = {worker_id: @worker.id}    token = JWT.encode(payload, 'secret')    render json: {success: true, jwt: token }  else    render json: {success: false, errors: "Authentication Failed :("}  endendprivatedef signup_params  params.require(:user).permit(:email, :password)end

Now back to the client side. If you want to handle errors, we should implement the useState hook for errors. You can then setError and display those errors on the form. If we have NO errors, we are going to put the JSON Web Token inside localStorage under the key of “token”.

axios.post('http://127.0.0.1:4000/login', {user: {email: email, password: password}}, {withCredentials: true}).then( response => {  if ( response.data.errors ) {    setError(response.data.errors)  }  else {    setError("")    localStorage.setItem("token", response.data.jwt)  }})

Congratulations. Now if you go to the dev console you should be able to see your token.

JSON Web Token inside Local Storage

Redirect After Authentication

Now that you can authenticate you will want to redirect the user. In Next.js you can use the useRouter hook to reroute the user.

import { useRouter } from 'next/router'

Add the method to your Login function.

const router = useRouter()

Create a component for the redirect to point to. Let’s call it a Profile component. If a web token is placed inside localStorage, add the redirect to profile from inside the post method in the Login component.

router.push("/profile")

Now when you successfully authenticate on login you, you should be redirected to /profile.

Retrieve User Info With JSON Web Tokens

At this point we have a login page, we can authenticate a user, receive a JSON Web Token with a user id encoded inside, and we can redirect to a profile page. We now need to populate that profile page with information.

In our Profile component, we need to retrieve this information with the useEffect hook and a post request. If all goes well we will:

  1. Put the JSON Web Token inside the header.
  2. Receive a response and assign this data to state.
  3. Display this data to the user.
useEffect(() => {axios.get("http://127.0.0.1:4000/profile", {headers: {'Authorization': `Basic ${localStorage.token}`}})  .then(response => {    setEmail(response.data.user.email)    setCountry(response.data.user.country)    setCity(response.data.user.city)    setPhoneNumber(response.data.user.phone_number)  })})

On the back end, with the help of the decode method we will:

  1. Decode the token to reveal the user id
  2. Find the user
  3. Return the email to the client
def show  decoded_hash = decode_token  if !decoded_hash.empty?      @worker = Worker.find(decoded_hash[0]['worker_id'])      render json: {success: true, user: { email: @worker.email }}  else    render json: {success: false}  endendprivatedef decode_token  auth_token = request.headers['Authorization'].split(' ')[1]  if auth_token  begin    JWT.decode(auth_token, 'secret', algorithm: 'HS256')  rescue JWT::DecodeError    []  endend

The user’s email should be returned in the body of that post request.

Ideally, the token will only contain the user id and will always be sent inside the header of a request.

Full-Stack Software Engineer and Lifelong Learner

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store