Implementing JWT Based Authentication in Go

Photo by Ricky Kharawala on Unsplash
Photo by Ricky Kharawala on Unsplash
We will start by implementing a login REST API, verifying Username/Password, generating JWT Token, and verifying JWT Token.

This chapter will introduce how to implement JWT-Token-Based authentication in Go. We will start by implementing a login REST API, verifying Username/Password, generating JWT Token, and verifying JWT Token.

The complete code for this chapter can be found in .

Creating a Go Project

First, let’s create a Go project. The package is github.com/xhhuango/waynestalk/gojwtexample.

% mkdir GoJwtExample
% cd GoJwtExample
GoJwtExample % go mod init github.com/xhhuango/waynestalk/gojwtexample

This project will eventually have two REST APIs, respectively login (POST /login) and obtaining user information (GET /info).

The login process is to obtain an username and a password from the frontend. Use this username to read user data from the database , and then check whether the password matches. If they match, a JWT token is generated and output to the frontend. The login process is complete.

The process of obtaining user data is to obtain the JWT token from a GET request header and verify the token. If the verification is successful, get the user ID from this token. Then, use this ID to read the user data in the database and output it to the frontend.

Creating a Database

Before starting JWT, let us first create the database. For convenience, we use GORM and SQLite in the project . We have introduced GORM and SQLite in the following articles for the reference of interested readers.

Use the following instructions to install GORM.

GoJwtExample % go get -u gorm.io/gorm
GoJwtExample % go get -u gorm.io/driver/sqlite

Add user.go and declare it User struct.

package main

import "gorm.io/gorm"

type User struct {
	gorm.Model
	Username string `json:"username"`
	Password string `json:"password"`
	Age      uint   `json:"age"`
}

Create repository.go as follows. We will use insertUser() to pre-insert two users as test data. In the login process, use findUserByUsername() to read user data based on username. Finally, in the process of obtaining user data, use findUserByID() to read user data based on user ID.

package main

func insertUser(username string, password string, age uint) (*User, error) {
    user := User{
        Username: username,
        Password: password,
        Age:      age,
    }
    if res := DB.Create(&user); res.Error != nil {
        return nil, res.Error
    }
    return &user, nil
}

func findUserByUsername(username string) (*User, error) {
    var user User
    if res := DB.Where("username = ?", username).Find(&user); res.Error != nil {
        return nil, res.Error
    }
    return &user, nil
}

func findUserByID(id uint) (*User, error) {
    var user User
    if res := DB.Find(&user, id); res.Error != nil {
        return nil, res.Error
    }
    return &user, nil
}

Create main.go as follows. In main(), we connect to the database and insert two user data in advance.

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

var DB *gorm.DB

func main() {
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    if err := db.AutoMigrate(&User{}); err != nil {
        panic(err)
    }
    DB = db

    _, _ = insertUser("david", "123", 30)
    _, _ = insertUser("sophia", "321", 25)
}

JWT-Token-Based Authentication

Before starting to implement the JWT token generation and verification, you must understand JWT-Token-Based Authentication process first. If you don’t know much, you can refer to the following article first.

Go has many third-party JWT packages. In this article, we use golang-jwt/jwt.

GoJwtExample % go get -u github.com/golang-jwt/jwt

Next, we use the following commands to generate a key to encrypt the JWT token.

% openssl rand -base64 172
FDr1VjVQiSiybYJrQZNt8Vfd7bFEsKP6vNX1brOSiWl0mAIVCxJiR4/T3zpAlBKc2/9Lw2ac4IwMElGZkssfj3dqwa7CQC7IIB+nVxiM1c9yfowAZw4WQJ86RCUTXaXvRX8JoNYlgXcRrK3BK0E/fKCOY1+izInW3abf0jEeN40HJLkXG6MZnYdhzLnPgLL/TnIFTTAbbItxqWBtkz6FkZTG+dkDSXN7xNUxlg==

Generating JWT

Create auth.go as follows. In generateToken(), we set the expiresAt of the token to 24 hours, and the Subject is the username of the user. Both of these are standard JWT fields, declared in jwt.StandardClaims. JWT also allows us to add customized fields, so we define UserID and set it as the user ID. In addition, we need to choose the encryption algorithm. We choose HS512, HMAC using SHA-512 hash algorithm. Finally, call SignedString() to generate a JWT token string.

package main

import (
    "errors"
    "fmt"
    "github.com/golang-jwt/jwt"
    "time"
)

var jwtKey = []byte("FDr1VjVQiSiybYJrQZNt8Vfd7bFEsKP6vNX1brOSiWl0mAIVCxJiR4/T3zpAlBKc2/9Lw2ac4IwMElGZkssfj3dqwa7CQC7IIB+nVxiM1c9yfowAZw4WQJ86RCUTXaXvRX8JoNYlgXcRrK3BK0E/fKCOY1+izInW3abf0jEeN40HJLkXG6MZnYdhzLnPgLL/TnIFTTAbbItxqWBtkz6FkZTG+dkDSXN7xNUxlg==")

type authClaims struct {
    jwt.StandardClaims
    UserID uint `json:"userId"`
}

func generateToken(user User) (string, error) {
    expiresAt := time.Now().Add(24 * time.Hour).Unix()
    token := jwt.NewWithClaims(jwt.SigningMethodHS512, authClaims{
        StandardClaims: jwt.StandardClaims{
            Subject:   user.Username,
            ExpiresAt: expiresAt,
        },
        UserID: user.ID,
    })

    tokenString, err := token.SignedString(jwtKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil
}

Verifying JWT

In auth.go, we add validateToken() to verify the JWT token. Call jwt.ParseWithClaims() to decrypt the JWT token. Here we noticed that instead of passing the key directly to jwt.ParseWithClaims(), it passes a function returning the key. In addition, in the function, remember to check whether the encryption algorithm is the same as the one we used for encryption.

After decryption, you will get a jwt.Token object. Check if jwt.Token.Valid is valid. If the verification passes, we can get the user ID and username from the claims.

func validateToken(tokenString string) (uint, string, error) {
    var claims authClaims
    token, err := jwt.ParseWithClaims(tokenString, &claims, 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 jwtKey, nil
    })
    if err != nil {
        return 0, "", err
    }

    if !token.Valid {
        return 0, "", errors.New("invalid token")
    }

    id := claims.UserID
    username := claims.Subject
    return id, username, nil
}

Integrating REST APIs

Next, we will implement two APIs and integrate the previous database and JWT code. We choose Gin as the web framework of the project. Readers who are not familiar with Gin can refer to the following article first.

Login API

Create handler.go as the code below. login() gets the username and password from the request body, and uses the username to read the user data from the database. Then, check whether the password matches. If they match, a JWT token is generated and output to the frontend.

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
)

func login(c *gin.Context) {
    var req struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "incorrect parameters",
        })
        return
    }

    user, err := findUserByUsername(req.Username)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{
            "error": fmt.Sprintf("user %s not found", req.Username),
        })
        return
    }

    if user.Password != req.Password {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "incorrect password",
        })
        return
    }

    token, err := generateToken(*user)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "token": token,
    })
}

In main(), run Gin web framework and add login() to the router.

package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "net/http"
    "strings"
)

var DB *gorm.DB

func main() {
    ...

    r := gin.Default()
    r.POST("/login", login)

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

Getting User Data API

In main(), we add a middleware to the router to verify the JWT token. After adding the middleware, any API we add will execute the middleware first. So, add /login before the middleware, and add /info after the middleware.

The verifyToken() middleware will obtain the JWT token from the request header and perform verification. If the verification is successful, the user ID and username obtained from the claims are set to the Gin context. After that, the handler can get the ID and username from the context.

package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "net/http"
    "strings"
)

var DB *gorm.DB

func main() {
    ...

    r := gin.Default()
    r.POST("/login", login)

    r.Use(verifyToken)
    r.GET("/info", getUserInfo)

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

func verifyToken(c *gin.Context) {
    token, ok := getToken(c)
    if !ok {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{})
        return
    }

    id, username, err := validateToken(token)
    if err != nil {
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{})
        return
    }

    c.Set("id", id)
    c.Set("username", username)

    c.Writer.Header().Set("Authorization", "Bearer "+token)

    c.Next()
}

func getToken(c *gin.Context) (string, bool) {
    authValue := c.GetHeader("Authorization")

    arr := strings.Split(authValue, " ")
    if len(arr) != 2 {
        return "", false
    }

    authType := strings.Trim(arr[0], "\n\r\t")
    if strings.ToLower(authType) != strings.ToLower("Bearer") {
        return "", false
    }

    return strings.Trim(arr[1], "\n\t\r"), true
}

func getSession(c *gin.Context) (uint, string, bool) {
    id, ok := c.Get("id")
    if !ok {
        return 0, "", false
    }

    username, ok := c.Get("username")
    if !ok {
        return 0, "", false
    }

    return id.(uint), username.(string), true
}

Add getUserInfo() to handler.go. It obtains the user ID from the Gin context, and reads user data from the database based on this ID. Finally, the user data is output to the frontend.

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
)

...

func getUserInfo(c *gin.Context) {
    id, _, ok := getSession(c)
    if !ok {
        c.JSON(http.StatusUnauthorized, gin.H{})
        return
    }

    user, err := findUserByID(id)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{})
        return
    }

    c.JSON(http.StatusOK, user)
}

Conclusion

JWT-Token-Based authentication is a very popular authentication process now. golang-jwt/jwt provides many convenient functions for us to generate and verify tokens, so we can see that the code in this chapter is not difficult.

4 comments
  1. How would I use this with golang’s html/template package to protect specific endpoints? I can login with curl and get a token, but how do I do that from a page?

Leave a Reply

Your email address will not be published. Required fields are marked *