Create a secured REST Api for a Next.js application using JWT and Golang

Published:

Overview

  • In this part we will be creating the JWT secured REST API
  • In the next part we will create the user facing Next.js application
  • In the last part we will add pre-render async api calls to our Next.js application

Source Code

Everything we are working on can be found on GitHub at https://github.com/jasonraimondi/nextjs-jwt-example. For this part, take a look in the api directory.

For this part, I am referencing the Echo JWT Recipe as an excellent starting point.

Outline the REST API

Let’s outline of the REST API we will be creating. We will have two open routes and one secure route that will require authentication.

GET  http://localhost:1323/api/unrestricted # NO AUTH REQUIRED
POST http://localhost:1323/api/login        # NO AUTH REQUIRED
GET  http://localhost:1323/api/restricted   # AUTHORIZATION HEADER REQUIRED 

Anyone will be able to access the /api/unrestricted endpoint. A user will be able to authenticate via a POST request containing a valid email and password to the /api/login endpoint and receiving a JWT. Authenticated users can then pass the JWT as an Authorization header to the /api/restricted endpoint to view the content. Any requests without the Authorization header will be denied.

Add the unrestricted/open endpoint

The first endpoint is just a really simple endpoint that returns a json object with a key and value of "message": "Success! The status is 200".

package main

import (
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
    "net/http"
)

func main() {
    e := echo.New()

    // logging and panic recovery middleware
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    // unrestricted route
    e.GET("/api/unrestricted", unrestricted)
    
    // listen on localhost:1323
    e.Logger.Fatal(e.Start(":1323"))
}

func unrestricted(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Success! The status is 200",
    })
}

Submit the GET requests to the unprotected endpoint

To demonstrate the endpoints that we have created, let’s request them on the CLI real quick. First we will hit the unrestricted endpoint.

curl -i localhost:1323/api/unrestricted

HTTP/1.1 200 OK

{"message":"Success! The status is 200"}

Create the login endpoint that authenticates a user and returns a jwt upon successful login

We are going to create a login endpoint that will take an email and password from login post request and validate the fields. In our case, we are hard coding the email and password. The only acceptable input would be email: rickety_cricket@example.com and pw: shhh!.

In a real implementation, we would be retrieving a user record in a database and validating the password.

package main

import (
    "github.com/dgrijalva/jwt-go"
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
    "net/http"
)

const jwtSecretKey = "my-super-secret-key"

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/api/unrestricted", unrestricted)
    // add the login route
    e.POST("/api/login", login)

    // add a restricted group
    r := e.Group("/api")
    // apply the jwt middleware to the route group
    r.Use(middleware.JWT([]byte(jwtSecretKey)))
    r.GET("/restricted", restricted)
    
    e.Logger.Fatal(e.Start(":1323"))
}

func login(c echo.Context) error {
    email := c.FormValue("email")
    password := c.FormValue("password")

    // in our case, the only "valid user and password" is 
    // user: rickety_cricket@example.com pw: shhh!
    // really, this would be connected to any database and 
    // retrieving the user and validating the password
    if email != "rickety_cricket@example.com" || password != "shhh!" {
        return echo.ErrUnauthorized
    }

    // create token
    token := jwt.New(jwt.SigningMethodHS256)

    // set claims
    claims := token.Claims.(jwt.MapClaims)
    // add any key value fields to the token 
    claims["email"] = "rickety_cricket@example.com"
    claims["exp"] = time.Now().Add(time.Hour * 72).Unix()

    // generate encoded token and send it as response.
    t, err := token.SignedString([]byte("secret"))
    if err != nil {
        return err
    }
    
    // return the token for the consumer to grab and save
    return c.JSON(http.StatusOK, map[string]string{
        "token": t,
    })
}

Please checkout the Echo JWT Cookbook & Middleware API for more information on the JWT implementation.

The login request failing (invalid credentials)

On a failure to login, either due an invalid password, or attempting to log into an invalid user (in our case, any user other than the hardcoded rickety_cricket@example.com), will result in a 401 Status Code and an Unauthorized message.

The following shows the result of the login request failing as a result of an invalid credentials being supplied.

curl -X POST -d 'email=rickety_cricket@example.com' \
             -d 'password=wrong-password!!' \
             localhost:1323/api/login

HTTP/1.1 401 Unauthorized

{"message":"Unauthorized"}

The login request succeeding

Next I am going to make a POST request with my email and password passed as form data to the API’s login page. The API will receive the request and begin the flow by verifying the user exists, and the password is correct.

The following shows the result of the login request succeeding and responding with a token.

curl -X POST -d 'email=rickety_cricket@example.com' \
             -d 'password=shhh!' \
             localhost:1323/api/login

HTTP/1.1 200 OK

{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZW1haWwiOiJyaWNrZXR5X2NyaWNrZXRAZXhhbXBsZS5jb20iLCJleHAiOjE1NjUxOTkzNzl9.BUSk39ZXXAUU6-L0sa3tlH_6vNnKIPWKoclOI1u85TA"}

If you run the token through a jwt decoder such as https://jwt.io you’ll get the decoded token.

{
  "admin": true,
  "email": "rickety_cricket@example.com",
  "exp": 1565199379
}

Create the restricted endpoint with the JWT middleware protection

I am using the Echo Framework JWT middleware, but the idea can be replicated in any language or framework.

We will use the middleware to create a restricted endpoint that only authenticated users can access.

package main

import (
    "github.com/dgrijalva/jwt-go"
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
    "net/http"
)

// Jwt signing key, this could be anything. Changing it would 
// effectively log out all users, but is not destructive
const jwtSecretKey = "my-super-secret-key"

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/api/unrestricted", unrestricted)

    // create a route group that will add the jwt middleware
    r := e.Group("/api")
    // apply the jwt middleware to the route group
    r.Use(middleware.JWT([]byte(jwtSecretKey)))
    r.GET("/restricted", restricted)
    
    // listen on localhost:1323
    e.Logger.Fatal(e.Start(":1323"))
}

func restricted(c echo.Context) error {
    // do a fancy dance to get the token's email
    user := c.Get("user").(*jwt.Token)
    claims := user.Claims.(jwt.MapClaims)
    email := claims["email"].(string)
    
    return c.JSON(http.StatusOK, map[string]string{
        "message": "hello email address: " + email,
    })
}

This restricted endpoint that responds with a JSON object of {"message": "hello email address: " + email}

The restricted request failing (no Authorization header)

Unsuccessfully request the restricted endpoint without any credentials

We are going to hit the restricted endpoint without providing any credentials. We receive a response status code of 400, and a message with the supplied error as expected.

curl -i localhost:1323/api/restricted

HTTP/1.1 400 Bad Request

{"message":"missing or malformed jwt"}

Append the Authorization header when requesting secure routes

The token from the successful login can now be used as the “Authorization” header for requesting secure (auth protected) endpoints.

curl -i localhost:1323/api/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZW1haWwiOiJyaWNrZXR5X2NyaWNrZXRAZXhhbXBsZS5jb20iLCJleHAiOjE1NjUxOTkzNzl9.BUSk39ZXXAUU6-L0sa3tlH_6vNnKIPWKoclOI1u85TA"

HTTP/1.1 200 OK

{"message":"hello email address: rickety_cricket@example.com"}

API Preview

Let’s take a look at the actual REST API we have implemented.

Continue to securing a Next.js application with JWT and a private route higher order component