Creating a REST API in Go with JSON Web Tokens

Creating a REST API in Go with JSON Web Tokens
Freepik
A guide to creating a REST API in Go with JSON Web Tokens.

Introduction

In this article, we will be discussing how to create a REST API using the Go programming language which uses JSON Web Tokens (JWT) for authentication. We will also be using the Gorilla web toolkit to help us create our API.

Prerequisites

Even if you have no prior knowledge of creating a REST API in Go, this tutorial will provide all the necessary information to get started. The tutorial covers creating the API, setting up JWT authentication, and creating the endpoints.

By the end of the tutorial, you should have a fully functioning REST API.

What is a REST API?

A REST API is an API that uses the REST (Representational State Transfer) architecture. The REST architecture is a way of designing APIs that uses HTTP requests to GET, PUT, POST, and DELETE data.

To learn more about APIs, check out my other article.

A Better Way to Build APIs: REST API vs RPC and gRPC vs GraphQL
Introduction REST application programming interface, RPC, gRPC, and GraphQL. These four technologies are often mentioned together, but what do they actually mean? More importantly, which one should you be using for your business? This article will compare REST API vs RPC and gRPC vs GraphQL in term…

What is JSON Web Token (JWT)?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret or a public/private key pair.

Getting Started

Run the go mod init command to initialize a new module in the current directory.

$ go mod init example.com/username/go-jwt

We will be using the Gorilla web toolkit to create our API. The Gorilla web toolkit is a set of tools that helps us create web applications in Go.

We will first need to install the Gorilla web toolkit. We can do this by running the following command:

$ go get github.com/gorilla/mux

Creating the API

We will start by creating a file called main.go. This file will contain our API code. We will then import the following packages:

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

    "github.com/dgrijalva/jwt-go"
    "github.com/gorilla/mux"
    "github.com/mitchellh/mapstructure"
)
  • The first package we are importing is the encoding/json package. This package provides us with functions for encoding and decoding JSON data.
  • The second package we are importing is the github.com/dgrijalva/jwt-go package. This package provides us with functions for creating and verifying JSON Web Tokens.
$ go get github.com/dgrijalva/jwt-go
  • The third package we are importing is the github.com/gorilla/mux package. This package provides us with functions for creating HTTP request handlers.
  • The fourth package we are importing is the github.com/mitchellh/mapstructure package. This package provides us with functions for mapping data structures.
$ go get github.com/mitchellh/mapstructure
  • The fifth and final package we are importing is the time package. This package provides us with functions for dealing with time.

Creating the User Struct

We will start by creating a struct to represent a user. We will call this struct User. This struct will have two fields: Username and Password.

type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

Creating the JWT Token Struct

We will then create a struct to represent a JWT token. We will call this struct JwtToken. This struct will have one field: Token.

type JwtToken struct {
    Token string `json:"token"`
}

Creating the Exception Struct

We will then create a struct to represent an exception. We will call this struct Exception. This struct will have one field: Message.

type Exception struct {
    Message string `json:"message"`
}

Creating the Response Struct

We will then create a struct to represent a response. We will call this struct Response. This struct will have one field: Data.

type Response struct {
    Data string `json:"data"`
}

Creating the JWT Key

We will then create a variable to hold our JWT key. We will call this variable JwtKey. This key will be used to sign our JWT tokens.

var JwtKey = []byte(os.Getenv("JWT_KEY"))

Creating the Users Variable

We will then create a variable to hold our list of users. We will call this variable Users.

var Users = []User{
    User{
        Username: "user1",
        Password: "password1",
    },
    User{
        Username: "user2",
        Password: "password2",
    },
}

Creating the CreateToken Function

We will then create a function to create a JWT token. We will call this function CreateToken. This function will take in two parameters: a http.ResponseWriter and a http.Request.

This function will first create a variable called user of type User. This variable will be used to hold the user data that is sent in the request body.

We will then decode the request body into this variable.

We will then create a new JWT token using the jwt.NewWithClaims function. This function takes in two parameters: the signing method and the claims.

The signing method is used to sign the token. We will be using the HS256 algorithm.

The claims are the data that will be encoded in the JWT token. We will be including the username and password of the user in the claims. We will also be including an expiration time for the token.

We will then sign the token using the JwtKey.

We will then encode the token into the JwtToken struct.

func CreateToken(w http.ResponseWriter, r *http.Request) {
    var user User
    _ = json.NewDecoder(r.Body).Decode(&user)
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "username": user.Username,
        "password": user.Password,
        "exp":      time.Now().Add(time.Hour * time.Duration(1)).Unix(),
    })
    tokenString, error := token.SignedString(JwtKey)
    if error != nil {
        fmt.Println(error)
    }
    json.NewEncoder(w).Encode(JwtToken{Token: tokenString})
}

Creating the ProtectedEndpoint Function

We will then create a function to handle HTTP requests to our protected endpoint. We will call this function ProtectedEndpoint. This function will take in two parameters: a http.ResponseWriter and a http.Request.

This function will first create a variable called params of type url.Values. This variable will be used to hold the URL parameters.

We will then get the token parameter from the URL parameters.

We will then create a new JWT token using the jwt.Parse function. This function takes in three parameters: the token string, a function for verifying the token, and the claims.

The function for verifying the token takes in two parameters: the token and the claims.

We will then check if the token is valid.

If the token is valid, we will decode the claims into the User struct.

We will then encode the User struct into the response.

If the token is not valid, we will encode the exception into the response.

func ProtectedEndpoint(w http.ResponseWriter, r *http.Request) {
    params := r.URL.Query()
    token, _ := jwt.Parse(params["token"][0], func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("There was an error")
        }
        return JwtKey, nil
    })
    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        var user User
        mapstructure.Decode(claims, &user)
        json.NewEncoder(w).Encode(user)
    } else {
        json.NewEncoder(w).Encode(Exception{Message: "Invalid authorization token"})
    }
}

Creating the ValidateMiddleware Function

We will then create a function to validate the JWT token. We will call this function ValidateMiddleware. This function will take in one parameter: a http.HandlerFunc.

This function will return a http.HandlerFunc.

This function will first get the authorization header from the HTTP request.

We will then check if the authorization header is present.

If the authorization header is present, we will split it on the space character.

We will then check if the split slice has a length of two.

If the split slice has a length of two, we will create a new JWT token using the jwt.Parse function. This function takes in three parameters: the token string, a function for verifying the token, and the claims.

The function for verifying the token takes in two parameters: the token and the claims.

We will then check if the token is valid.

If the token is valid, we will call the next handler.

If the token is not valid, we will encode the exception into the response.

If the authorization header is not present, we will encode the exception into the response.

func ValidateMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authorizationHeader := r.Header.Get("authorization")
        if authorizationHeader != "" {
            bearerToken := strings.Split(authorizationHeader, " ")
            if len(bearerToken) == 2 {
                token, error := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) {
                    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                        return nil, fmt.Errorf("There was an error")
                    }
                    return JwtKey, nil
                })
                if error != nil {
                    json.NewEncoder(w).Encode(Exception{Message: error.Error()})
                    return
                }
                if token.Valid {
                    next.ServeHTTP(w, r)
                } else {
                    json.NewEncoder(w).Encode(Exception{Message: "Invalid authorization token"})
                }
            }
        } else {
            json.NewEncoder(w).Encode(Exception{Message: "An authorization header is required"})
        }
    })
}

Creating the Router

We will then create a new router using the mux.NewRouter function.

We will then add a route for our authenticate endpoint. This endpoint will be used to create JWT tokens.

We will then add a route for our protected endpoint. This endpoint will be used to test our JWT tokens.

We will then add a route for our health check endpoint. This endpoint will be used to test if our API is up and running.

func main() {
    router := mux.NewRouter()
    fmt.Println("Starting the application...")
    router.HandleFunc("/authenticate", CreateToken).Methods("POST")
    router.HandleFunc("/protected", ValidateMiddleware(ProtectedEndpoint)).Methods("GET")
    router.HandleFunc("/health-check", HealthCheck).Methods("GET")
    log.Fatal(http.ListenAndServe(":12345", router))
}

Creating the HealthCheck Function

We will then create a function to handle HTTP requests to our health check endpoint. We will call this function HealthCheck. This function will take in two parameters: a http.ResponseWriter and a http.Request.

This function will encode the response into the response.

func HealthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(Response{Data: "API is up and running"})
}

The full code and instructions on how to run the program are available on my GitHub repository. Make sure to give it a star if you like it!

narasimman-tech/Creating-a-REST-API-in-Go-with-JSON-Web-Tokens at main · lakshminarasimmanv/narasimman-tech
Resources, Code Snippets, and Code Examples for the articles on Narasimman Tech and Medium. - narasimman-tech/Creating-a-REST-API-in-Go-with-JSON-Web-Tokens at main · lakshminarasimmanv/narasimman-...

Note: Run the go mod tidy command to update the go.mod file to use the latest minor or patch releases for all of the dependencies that are mentioned in the file.

$ go mod tidy

Conclusion

In this article, we have discussed how to create a REST API using the Go programming language which uses JSON Web Tokens (JWT) for authentication. We have also used the Gorilla web toolkit to help us create our API.


Subscribe to my weekly newsletter for the latest insights on Golang, DevOps, Cloud-Native, Linux, Kubernetes, Networking, Security, and more.

Follow me on Twitter!


Additional Resources and References: