JWT Authorization in Golang

BY Shrivatsa Upadhye
Dec 20 2019
10 Min

Authorization decides whether a particular user/service is allowed access to a particular route, service or resource. This is where JWT comes into the picture. It has a small overhead and it works across different domains.

JWT Concepts

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. It’s one of the most popular ways of authentication. It’s an encoded string that can contain any amount of data and it is cryptographically signed (by the server side). No middleman can modify it.

NOTE: JWT assures data ownership and not encryption. So it’s always best practice to use HTTPS with JWT.

The JWT token consists of 3 parts:

  1. Header
  2. Payload
  3. Signature

It contains information about the alogirthm used to generate the signature. These could be HMAC, SHA256 or RSA. It is Base64 encoded to form the first part of the JWT.

The header can also hold an additional information like “kid” [key id]. This is particularly useful when there are multiple keys used to sign different kinds of tokens within your application and have to look up the right one to verify the signature.

In the example, in next sections, for the user service from ACME Shop app, I will be using 2 keys, one for access_token and another one for refresh_token.

Payload

This is the second part of the token. It contains claims, which are statements/fields about an entity and any additional data. In JWT, there are 3 main types of claims - registered, public and private.

The most widely used claims are iss, exp and sub.

iss - issuer is used to identify the issuer of the the JWT.

exp - expiration time of the JWT

sub - subject identifies the principal

This payload is also Base64 encoded.

Signature

This is created from the encoded header, encoded payload, algorithm in the header and a secret key.

The final output is 3 base64 URL strings separated by dots that can be sent in HTTP requests.

It looks like this:

eyJhbGciOiJIUzI1NiIsImtpZCI6InNpZ25pbl8xIiwidHlwIjoiSldUIn0.eyJVc2VybmFtZSI6ImVyaWMiLCJleHAiOjE1NzA3NjI5NzksInN1YiI6IjVkOTNlMTFjNmY4Zjk4YzlmYjI0ZGU0NiJ9.n70EAaiY6rbH1QzpoUJhx3hER4odW8FuN2wYG1sgH7g

The Process

Let’s look at how this process works. When the user authenticates with their credentials, a JWT is returned. Precaution must be taken while storing these tokens on the client side (i.e, browser). Do not use session storage for these tokens.

When the next API call is made from the client side, the JWT must be sent along with request in the Authorization header using the Bearer schema. Authorization: Bearer <token>

This means on the user/auth service, the routes would check if Authorization exists within the header.

JWT Flow

Let’s now look at various methods that need to implemented for JWT to work within golang.

User Login

When the user successfully logs in, we need to return a JWT.

This involves generating an access_token and a refresh_token.

I will only show the main bits of the function here. You can find the entire codebase here

func GenerateTokenPair(username string, uuid string) (string, string, error) {

	tokenString, err := GenerateAccessToken(username, uuid)
	if err != nil {
		return "", "", err
	}

	// Create Refresh token, this will be used to get new access token.
	refreshToken := jwt.New(jwt.SigningMethodHS256)
	refreshToken.Header["kid"] = "signin_2"

	expirationTimeRefreshToken := time.Now().Add(15 * time.Minute).Unix()

	rtClaims := refreshToken.Claims.(jwt.MapClaims)
	rtClaims["sub"] = uuid
	rtClaims["exp"] = expirationTimeRefreshToken

	refreshTokenString, err := refreshToken.SignedString(RtJwtKey)
	if err != nil {
		return "", "", err
	}

	return tokenString, refreshTokenString, nil
}

The GenerateAccessToken method is similar to the refreshToken generation but uses a different kid (signin_1). Also, the expiration time is just 5 minutes instead of 15 min as shown above in case of refreshToken.

Note: There is no extraction of Authorization header here as this is user login method. Everytime the user logs in with credentials, we have to issue them a new set of access_token and refresh_token.

Authorization Middleware

Next, when the user makes an API request with JWT, we first verify if the Bearer token exists for every route which needs user info. This verification can be done via middleware within Golang.

authGroup := router.Group("/")
	// Added
	authGroup.Use(auth.AuthMiddleware())
	{
		authGroup.GET("/users", service.GetUsers)
		authGroup.GET("/users/:id", service.GetUser)
		authGroup.DELETE("/users/:id", service.DeleteUser)
		authGroup.POST("/logout", service.LogoutUser)
	}

Another point to remember when implementing JWT is that once the token is issued it can be used till expiration time. So how do we handle logout of a user session?

This is where you will have to maintain a blacklist of all the tokens. If the user chooses to logout from an active session, we will have to keep this within the blacklist and check if it exists in the list everytime an API call is made. If it exists then we return 401 Unauthorized error.

This must be a part of the AuthMiddleware. I have used redis database for this. These access_token should be maintained within redis only till their expiration time. After that they would be eitherways invalidated during JWT verification step.

Within the logout method, we need to add a step to invalidate the token.

func LogoutUser(c *gin.Context) {

	token := c.GetHeader("Authorization")
	if token == "" {
		logger.Logger.Errorf("Authorization token was not provided")
		c.JSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "message": "Authorization Token is required"})
		c.Abort()
		return
	}

	extractedToken := strings.Split(token, "Bearer ")

    // This method will add the token to the redis db
	err := auth.InvalidateToken(extractedToken[1])
	if err != nil {
		c.Abort()
		return
	}

	c.JSON(http.StatusAccepted, gin.H{"status": http.StatusAccepted, "message": "Done"})

}

The data is set within redis like this status := db.RedisClient.Set(tokenString, tokenString, expiryTime)

Also, the refresh_token returned to the user during login, has a longer expiration time than access_token. This is because of the fact that the refresh_token is used to obtain new access_token. Access tokens are the ones which are used for authorization. If the user sends refresh token instead of access token, the middleware should return an error.

// AuthMiddleware checks if the JWT sent is valid or not. This function is involked for every API route that needs authentication
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientToken := c.GetHeader("Authorization")
        if clientToken == "" {
            logger.Logger.Errorf("Authorization token was not provided")
            c.JSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "message": "Authorization Token is required"})
            c.Abort()
            return
        }

        claims := jwt.MapClaims{}

        extractedToken := strings.Split(clientToken, "Bearer ")

        // Verify if the format of the token is correct
        if len(extractedToken) == 2 {
            clientToken = strings.TrimSpace(extractedToken[1])
        } else {
            logger.Logger.Errorf("Incorrect Format of Authn Token")
            c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": "Incorrect Format of Authorization Token "})
            c.Abort()
            return
        }

        foundInBlacklist := IsBlacklisted(extractedToken[1])

        if foundInBlacklist == true {
            logger.Logger.Infof("Found in Blacklist")
            c.JSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "message": "Invalid Token"})
            c.Abort()
            return
        }

        // Parse the claims
        parsedToken, err := jwt.ParseWithClaims(clientToken, claims, func(token *jwt.Token) (interface{}, error) {
            return AtJwtKey, nil
        })

        if err != nil {
            if err == jwt.ErrSignatureInvalid {
                logger.Logger.Errorf("Invalid Token Signature")
                c.JSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "message": "Invalid Token Signature"})
                c.Abort()
                return
            }
            c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": "Bad Request"})
            c.Abort()
            return
        }

        if !parsedToken.Valid {
            logger.Logger.Errorf("Invalid Token")
            c.JSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "message": "Invalid Token"})
            c.Abort()
            return
        }

        c.Next()
    }
}

The IsBlacklisted is a simple function, which checks if the token exists in the redis db or not.

func IsBlacklisted(tokenString string) bool {

	status := db.RedisClient.Get(tokenString)

	val, _ := status.Result()

	if val == "" {
		return false
	}

	return true
}

Renew access_token

As we learnt in the previous section that access_token have a shorter validatity, we need to add a method to reissue a new access_token. To do this we will add a new route called /renew-token. With this request the user is expected to send their refresh_token that was obtained during the login step.

func RenewAccessToken(c *gin.Context) {

	var tokenRequest auth.RefreshTokenRequestBody

	err := c.ShouldBindJSON(&tokenRequest)
	if err != nil {
		message := err.Error()
		c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": message})
		return
	}

	valid, id, _, err := auth.ValidateToken(tokenRequest.RefreshToken)
	if valid == false || err != nil {
		message := err.Error()
		c.JSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "message": message})
		c.Abort()
		return
	}

	if valid == true && id != "" {

		var user auth.UserResponse

		// Retreive the username from users DB. This will verify if the user ID passed with JWT was legit or not. 
		error := db.Collection.FindId(bson.ObjectIdHex(id)).One(&user)	

		if error != nil {
			message := "User " + error.Error()
			logger.Logger.Errorf(message)
			c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": "Invalid refresh token"})
			c.Abort()
			return
		}

		newToken, err := auth.GenerateAccessToken(user.Username, id)
		if err!=nil {
			logger.Logger.Errorf(err.Error())
			c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": "Cannot Generate New Access Token"})
			c.Abort()
			return
		}
		c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "access_token": newToken, "refresh_token": tokenRequest.RefreshToken})
		c.Abort()
		return
	}

	c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": "Error Found "})

}

The ValidateToken method should check for key and signature validity.

// ValidateToken is used to validate both access_token and refresh_token. It is done based on the "Key ID" provided by the JWT
func ValidateToken(tokenString string) (bool, string, string, error) {

	var key []byte

	var keyID string

	claims := jwt.MapClaims{}

	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {

		keyID = token.Header["kid"].(string)
		// If the "kid" (Key ID) is equal to signin_1, then it is compared against access_token secret key, else if it
		// is equal to signin_2 , it is compared against refresh_token secret key.
		if keyID == "signin_1" {
			key = AtJwtKey
		} else if keyID == "signin_2" {
			key = RtJwtKey
		}
		return key, nil
	})

	// Check if signatures are valid.
	if err != nil {
		if err == jwt.ErrSignatureInvalid {
			logger.Logger.Errorf("Invalid Token Signature")
			return false, "", keyID, err
		}
		return false, "", keyID, err
	}

	if !token.Valid {
		logger.Logger.Errorf("Invalid Token")
		return false, "", keyID, err
	}

	return true, claims["sub"].(string), keyID, nil
}

Verify token

When service to service API calls are made, the JWT needs to be verified first with the auth/user service. This method assumes that the API gateway does not modify the JWT token with a self generated token.

The route /verify-token expects the internal service to provide the access_token as part of the request. The method would be exactly similar to the ValidateToken method shown in the previous section but it would return a 200 OK , if the token is valid.

Once these primary methods are setup, you can build your application and see this in action.

Conclusion

JWT is most popular authorization method because of it’s simplicity. It can be used to exchanged large amounts of data and is easy to parse. The use cases for user to service and service to service authorization must be handled. There might be a necessity to add a caching layer, like redis db, to manage blacklisted access tokens. On the client side, store the tokens as httpOnly cookie.

If you’re interested in more details or have any thoughts on using JWT you want to share with us, connect with me or the team on Twitter!