Go JWT Authentication Middleware

Go JWT Authentication Middleware

Hi, I am Amit Shekhar, Co-Founder @ Outcome School • IIT 2010-14 • I have taught and mentored many developers, and their efforts landed them high-paying tech jobs, helped many tech companies in solving their unique problems, and created many open-source libraries being used by top companies. I am passionate about sharing knowledge through open-source, blogs, and videos.

In this blog, we will learn the implementation of JWT Authentication Middleware in Go (Golang).

First of all, I must mention that the use of JWT(JSON Web Token) in the OAuth 2.0 protocol is a battle-tested way of authenticating users.

While most of the token-based authentication implementation needs a database query to verify the validity of the granted token, JWT doesn't need the database query to verify. Moreover, with JWT, we can encode some data like User ID as a payload and can be decoded to get the data when needed.

We all have been using it for a long time in projects with different languages like Java, JavaScript, Python, etc. Today, we are going to do the same in the Go language.

This article was originally published at Outcome School.

Let's Go :)

I will be using the below-mentioned project for the implementation part. The project follows a clean architecture in Go Language. You can find the complete code for the implementation of JWT Authentication Middleware mentioned in the blog in the project itself.

Link to the project: Go Backend Clean Architecture.

We will be using the following package to use JWT in Go:

go get -u github.com/golang-jwt/jwt/v4

Then, import it in your code:

import "github.com/golang-jwt/jwt/v4"

First of all, we have to create a file jwt_custom.go in the domain package as below:

package domain

import (
    "github.com/golang-jwt/jwt/v4"
)

type JwtCustomClaims struct {
    Name string `json:"name"`
    ID   string `json:"id"`
    jwt.StandardClaims
}

type JwtCustomRefreshClaims struct {
    ID string `json:"id"`
    jwt.StandardClaims
}

and suppose, we have a struct User as below:

package domain

type User struct {
    ID       string
    Name     string
}

We also have to put the secret key and expiry time for both the access token and refresh token in our configuration files such as .env or config.json.

ACCESS_TOKEN_EXPIRY_HOUR = 2
REFRESH_TOKEN_EXPIRY_HOUR = 168
ACCESS_TOKEN_SECRET=access_token_secret
REFRESH_TOKEN_SECRET=refresh_token_secret

I have used the .env file for this and then loads it into the struct Env.

type Env struct {
    AccessTokenExpiryHour  int
    RefreshTokenExpiryHour int
    AccessTokenSecret      string
    RefreshTokenSecret     string
}

Then, we have to create a file tokenutil.go. I have created this inside the tokenutil package under the internal package.

In this file, we have the following functions:

  • CreateAccessToken(user *domain.User, secret string, expiry int)
  • CreateRefreshToken(user *domain.User, secret string, expiry int)
  • IsAuthorized(requestToken string, secret string)
  • ExtractIDFromToken(requestToken string, secret string)

Let's understand what these functions do one by one.

CreateAccessToken(user *domain.User, secret string, expiry int)

This function takes three parameters: user, access secret, and expiry.

And it creates a token by encoding the payload that consists of the user Name and ID with the given expiry time signed with the given access secret.

CreateRefreshToken(user *domain.User, secret string, expiry int)

This function also takes three parameters: user, refresh secret, and expiry.

And it creates a token by encoding the payload that consists of the user ID with the given expiry time signed with the given refresh secret.

IsAuthorized(requestToken string, secret string)

This function does the task of checking if the given token is authorized or not. That's it.

ExtractIDFromToken(requestToken string, secret string)

This function decodes and extracts the ID that was encoded while creating the token.

The complete code is present inside this tokenutil.go file.

package tokenutil

import (
    "fmt"
    "time"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    jwt "github.com/golang-jwt/jwt/v4"
)

func CreateAccessToken(user *domain.User, secret string, expiry int) (accessToken string, err error) {
    exp := time.Now().Add(time.Hour * time.Duration(expiry)).Unix()
    claims := &domain.JwtCustomClaims{
        Name: user.Name,
        ID:   user.ID,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: exp,
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    t, err := token.SignedString([]byte(secret))
    if err != nil {
        return "", err
    }
    return t, err
}

func CreateRefreshToken(user *domain.User, secret string, expiry int) (refreshToken string, err error) {
    claimsRefresh := &domain.JwtCustomRefreshClaims{
        ID: user.ID,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(time.Hour * time.Duration(expiry)).Unix(),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsRefresh)
    rt, err := token.SignedString([]byte(secret))
    if err != nil {
        return "", err
    }
    return rt, err
}

func IsAuthorized(requestToken string, secret string) (bool, error) {
    _, err := jwt.Parse(requestToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(secret), nil
    })
    if err != nil {
        return false, err
    }
    return true, nil
}

func ExtractIDFromToken(requestToken string, secret string) (string, error) {
    token, err := jwt.Parse(requestToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(secret), nil
    })

    if err != nil {
        return "", err
    }

    claims, ok := token.Claims.(jwt.MapClaims)

    if !ok && !token.Valid {
        return "", fmt.Errorf("Invalid Token")
    }

    return claims["id"].(string), nil
}

Next, we will create the middleware package. In this, we will create jwt_auth_middleware.go file.

Here, we need to understand that, based on the HTTP web framework that we are using, we will put the handler function corresponding to that framework. In our case, we are using the Gin HTTP web framework. So we will use the gin.HandlerFunc.

But the logic will be the same inside that function whatever framework we choose except the modification related to that framework.

The code in that file will be as below:

package middleware

import (
    "net/http"
    "strings"

    "github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
    "github.com/amitshekhariitbhu/go-backend-clean-architecture/internal/tokenutil"
    "github.com/gin-gonic/gin"
)

func JwtAuthMiddleware(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.Request.Header.Get("Authorization")
        t := strings.Split(authHeader, " ")
        if len(t) == 2 {
            authToken := t[1]
            authorized, err := tokenutil.IsAuthorized(authToken, secret)
            if authorized {
                userID, err := tokenutil.ExtractIDFromToken(authToken, secret)
                if err != nil {
                    c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: err.Error()})
                    c.Abort()
                    return
                }
                c.Set("x-user-id", userID)
                c.Next()
                return
            }
            c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: err.Error()})
            c.Abort()
            return
        }
        c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: "Not authorized"})
        c.Abort()
    }
}

It takes the access secret key as an input parameter.

First, it extracts the token from the header of the request.

Then, it checks if the token is authorized or not, and it returns an error if not authorized.

If it is authorized, it extracts the UserID from the token that we have put in while creating the access token. And then put it into the context of the HTTP web framework used so that we can extract it easily when needed later in the request flow.

We can get the UserID from the HTTP Web Framework Context as below:

userID := c.GetString("x-user-id")

Then, we can use this middleware as below:

router.Use(middleware.JwtAuthMiddleware(env.AccessTokenSecret))

When we want to generate the access token, we can call:

accessToken, err := tokenutil.CreateAccessToken(user, secret, expiry)

For generating the refresh token, we can call:

refreshToken, err := tokenutil.CreateRefreshToken(user, secret, expiry)

This is how we can use JWT Authentication Middleware in Go (Golang).

I will highly recommend going through the project and checking the end-to-end flow of the implementation of JWT Authentication Middleware.

Link to the project: Go Backend Clean Architecture.

That's it for now.

Thanks

Amit Shekhar

Co-Founder @ Outcome School

You can connect with me on:

Read all of our blogs here.