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 .
Table of Contents
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
Tnx Wayne, very helpful. Really like how readable your code is.
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?
Sorry, I can’t help your for this because I’ve never used Golang’s html/template package before.
Thanks for this.
Much appreciated