GO JWT Authentication and Authorization Tutorial


GO

Reviewer: Deepak Prasad

Introduction

In this article, I will show how to authenticate a user and authorize access to resources in our application. Authenticate and authorize are two different concepts that work together to make applications more secure.

 

Authentication

Authentication refers to the process where a user enters their email and password and sends the data to the server. The server checks validity of the data and sends a response to the client . In this article, users will be able to sign up and login which will be the authentication part.

 

Authorization

Authorization refers to the process of checking users credentials after authentication, storing the user credentials in a request token and giving access to resources in the system based on data found in the token part.We will use middleware to give access based on the data in the token.

 

Token structure

A complete token is made up of three parts namely header, payload and signature.

Header

Identifies the algorithm to be used to generate a signature. In this article we will use the HS256 algorithm.

{
   "alg": "HS256",
   "typ": "JWT"
}

 

Payload

Stores information about a user like email, id, role subject and any other information related to the user.

Example

{
   "sub": "1234567890",
   "name": "john Doe",
   "admin": "true"
}

 

Signature

Validates the token by encoding the header and the payload using the Base64url Encoding RFC and concatenates the two together with a period separator.

Example

HMAC_SHA256(
   secret,
   base64urlEncoding(header) + '.' +
   base64urlEncoding(payload)
)

Prerequisites

  1. Go runtime installed
  2. Basic Go knowledge
  3. PostgreSQL installed
  4. Code editor of your choice , in this articles we will use Visual Studio Code

 

Application Structure

In this article the application will be made up of three modules namely controller, middleware and model. The controller module will host code for the Gin Web framework and route HTTP requests from the client to the server. The middleware will host code for authorizing HTTP requests coming from the client. The model module will host code for PostgreSQL database connection and all database operations.

GO JWT Authentication and Authorization Tutorial

Navigate to your working directory add the following commands in the terminal to set up our application.

Create application directory

mkdir jwt-demo && cd jwt-demo

Create modules

mkdir controller middleware model

Create main.go and .env file

touch main.go .env

Initialize go module

go mod init example.com/jwt-demo

Install dependencies

go get -u github.com/gin-gonic/gingo get -u gorm.io/gormgo get -u gorm.io/driver/postgresgo get -u github.com/golang-jwt/jwt/v4
go get github.com/joho/godotenv

To start us off, we need to define environmental variables such as database port, host and password. Add the below code in the .env file.

SECRET=topsecret
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_NAME=postgres
DB_PASSWORD=mypassword

Please note that these environmental variables will not be the same as your credentials, especially the DB_USER ,DB_NAME and DB_PASSWORD.  Replace these with your own values that you use to access your postgres database.

Next thing to do is to add code for database operations and user model in the database.go file.

Navigate into the database module and create a database.go file

cd model && touch database.go

model/database.go

package model
 
import (
   "fmt"
   "log"
   "os"
 
   "github.com/jinzhu/gorm"
   _ "github.com/jinzhu/gorm/dialects/postgres"
   "github.com/joho/godotenv"
   "golang.org/x/crypto/bcrypt"
)
 
var DB *gorm.DB
var err error
 
var secretKey = envVariable("SECRET")
 
// ############ Application DB ###########################
func SetDBClient() {
   var (
       host     = envVariable("DB_HOST")
       port     = envVariable("DB_PORT")
       user     = envVariable("DB_USER")
       dbname   = envVariable("DB_NAME")
       password = envVariable("DB_PASSWORD")
   )
 
   dns := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
       host,
       port,
       user,
       dbname,
       password,
   )
 
   DB, err = gorm.Open("postgres", dns)
   DB.AutoMigrate(User{})
   if err != nil {
       fmt.Println(err)
   }
   fmt.Println("Connection to the database is successful")
 
}
func envVariable(key string) string {
   err := godotenv.Load(".env")
   if err != nil {
       log.Fatalf("Error loading .env file")
   }
   return os.Getenv(key)
}
 
// ################ Application Model ####################
type User struct {
   Email    string `json:"email" gorm:"unique"`
   Password string `json:"password"`
}
 
func (u *User) GeneratePasswordHarsh() error {
   bytes, err := bcrypt.GenerateFromPassword([]byte(u.Password), 14)
   u.Password = string(bytes)
   return err
}
func (u *User) CheckPasswordHarsh(password string) bool {
   err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
   return err == nil
}

In this file , we start by importing different packages for database interaction and managing user data.

  1. github.com/jinzhu/gorm  Database object relational mapper for postgres operations.
  2. github.com/jinzhu/gorm/dialects/postgres : Gives us access to database operations such as creating a database instance
  3. github.com/joho/godotenv : For accessing environment variables in the .env file
  4. golang.org/x/crypto/bcrypt: For password hashing when users sign up.

We start by declaring an instance of a database using the var DB *gorm.DB. This will be assigned a database client after a successful database client creation. We also declare an error  variable that gets assigned a value when an error occurs while working with the database client. Lastly , we define a secret variable that stores the secret key defined in the .env file.

Next we define a User struct with email and password fields. The User struct has methods for generating a password harsh using the GenerataPasswordharsh() function. It also has the CheckPasswordHash() function that validates harshed passwords.

We also have the SetDBClient() function that creates a new database client using environmental variables from the .env file. The SetDBClient() function starts with a capital letter S meaning that its an exported function that will be used in the main.go file.The SetDBClient() function uses the envVariable() function to get environmental variables from the .env file.

 

controller/gin.go

The main responsibility of the code in this file is to route POST and GET requests to their respective handler functions. This file host three handler functions namely:

  1. <b>Signup()</b> , for creating a new user
  2. <b>Login()</b> for authenticating and authorizing  access to the application.
  3. <b>Resources() </b>a function that returns registered users in the application as resources. Only authenticated and authorized users can get access to this resource

These function takes as an argument a gin context c *gin.Context.The context is used HTTP requests operations such getting data, returning responses status code to the client.

This file starts off by importing the model, gin and jwt for database interaction, HTTP routing and token creating respectively.

 

Signup() handler function

package controller
 
import (
   "net/http"
   "os"
   "time"
 
   "example.com/jwt-demo/model"
   "github.com/gin-gonic/gin"
   "github.com/golang-jwt/jwt"
)
 
func Signup(c *gin.Context) {
   var reqUser model.User
   // Bind user data
   if err := c.ShouldBind(&reqUser); err != nil {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": err,
       })
       return
   }
   // Check user exists in db
   var dbUser model.User
   model.DB.Where("email =?", reqUser.Email).First(&dbUser)
   if dbUser.Email != "" {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": "user with email found, login",
       })
       return
   }
   // Harsh user password
   err := reqUser.GeneratePasswordHarsh()
   if err != nil {
       c.JSON(http.StatusInternalServerError, gin.H{
           "error": "unable to harsh password",
       })
       return
   }
 
   // Add user into the database
   res := model.DB.Create(&reqUser)
   if res.Error != nil {
       c.JSON(http.StatusInternalServerError, gin.H{
           "error": "failed to create user",
       })
       return
   }
   c.JSON(http.StatusOK, gin.H{
       "user": reqUser,
   })
}

 

The signup handler function performs the following operations.

  1. Binds data from the client with an existing user model. If the binding is not successful , an error is returned to the client using the gin context.
  2. Check if a user exists. After a successful binding, we check if a user with the email provided exists in the database. If a user exists, we return to the client a message indicating a user already exists.
  3. Generate password harsh. If no user is found with the email from the client, we use the GenerataPassrdHarsh() method from the model.User struct to generate a harshed password and assign the password to the password field of the model.User struct.
  4. Save user into the database. After a successful password harshing, the new user is saved into the database using the res := models.DB.Create() statement.

Please note in the signup operation, there is no interaction with the JWT tokens because we are only registering a user hence no token required. Login section is of interest to use. This is where we generate a token.

 

Login handler function

The login function is responsible for creating tokens for users who have already registered.

func Login(c *gin.Context) {
   var reqUser model.User
   if err := c.ShouldBind(&reqUser); err != nil {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": err,
       })
       return
   }
   // Get user from model
   var dbUser model.User
   model.DB.Where("email =?", reqUser.Email).First(&dbUser)
 
   if dbUser.Email == "" {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": "Invalid email or password",
       })
       return
   }
   if dbUser.CheckPasswordHarsh(reqUser.Password) {
 
       token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
           "sub": dbUser.Email,
           "exp": time.Now().Add(time.Minute * 10).Unix(),
       })
 
       // Sign and get the complete encoded token as a string using the secret
       tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
 
       if err != nil {
           c.JSON(http.StatusBadRequest, gin.H{
               "error": err.Error(),
           })
           return
       }
       c.SetSameSite(http.SameSiteLaxMode)
       c.SetCookie("Authorization", tokenString, 3600*24*30, "", "", false, true)
       c.JSON(http.StatusOK, gin.H{
           "user": dbUser,
       })
 
   } else {
       c.JSON(http.StatusNotFound, gin.H{
           "error": "Invalid email or password",
       })
   }
}

Below are the steps authenticating and authorizing a user.

  1. Binds data from the client with an existing user model. If the binding is not successful , an error is returned to the client using the gin context.
  2. Check if a user exists. After a successful binding, we check if a user with the email provided exists in the database. If a user exists, we move to the next step to validate the harshe password.
  3. Validate password using the CheckPasswordHarsh() function. This is the main focus of our tutorial. We generate tokens using the JWT package. The jwt package has the NewWithClaims() function that takes in a signing method in this case we are using HS256 and a claim as the second argument. Claims are statements about an entity in this case the use email and expiration date for the token. For more information about claims read here.The NewWithClaims() function returns a token to the caller.After a successful token creation, we sign the token using the secret key from the .env file using the statement tokenString, err := token.SignedString([]byte(os.Getenv("SECRET"))).
  4. Set cookies using gin context. The generated token gets set in the response headers using the SetCookie() function. The statement c.SetCookie("Authorization", tokenString, 3600*24*30, "", "", false, true) create a cookie of type Authorization and use the generated tokenString to authorize it.After a successful login, the user details are returned to the client.

 

Resources handler function

This function returns all the users in the application to the client. Later on , we will add a middleware that will allow users with valid tokens to access this resource.

func Resources(c *gin.Context) {
   var users []model.User
   res := model.DB.Find(&users)
   if res.Error != nil {
       c.JSON(http.StatusOK, gin.H{
           "message": "error fetching users",
       })
       return
   }
 
   c.JSON(http.StatusOK, gin.H{
       "users": users,
   })
}

 

middleware/authorization.go

This file hosts code for validating a token from the client. Being a middleware, HTTP requests will have to pass through this middleware and return to the respective handler function only if the token validates successfully , otherwise a response not authorized will be returned to the client. In the middleware module , add the below code.

package middleware
 
import (
   "fmt"
   "net/http"
   "os"
   "time"
 
   "example.com/jwt-demo/model"
   "github.com/gin-gonic/gin"
   "github.com/golang-jwt/jwt"
)
 
func Authorize(c *gin.Context) {
   tokenString, err := c.Cookie("Authorization")
   if err != nil {
       c.AbortWithStatus(http.StatusUnauthorized)
       return
   }
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
       if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
           return nil, fmt.Errorf("unexpected signing method: %v", token.Header["sub"])
       }
       return []byte(os.Getenv("SECRET")), nil
   })
   if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
       if float64(time.Now().Unix()) > claims["exp"].(float64) {
           c.AbortWithStatus(http.StatusUnauthorized)
           return
       }
       var user model.User
       model.DB.Where("email =?", claims["sub"]).First(&user)
 
       if user.Email == "" {
           c.AbortWithStatus(http.StatusUnauthorized)
           return
       }
       c.Set("user", user)
       c.Next()
   } else {
       c.AbortWithStatus(http.StatusUnauthorized)
       return
   }
}

The Authorize() middleware takes as an argument the gin context. The context is used to access the token using the <b>tokenString, err := c.Cookie("Authorization") </b>statement.If the token is missing in the cookie, we abort the operation and return to the user a HTTP response with an not authorized status code.

If the token exists, we parse it using the token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error). The parsing step also confirms that we are using the correct signing method, if a different signing method is found, we return an error to the client. If the correct signing method is found, we return our secret key from the .env file as bytes using return []byte(os.Getenv("SECRET")), nil statement.

The next step is to check the validity of the token by checking the data in the claims. If the token is valid, we ensure that the token has not expired. If the token is not expired we pull the email from the claim using the claims[“sub”] statement. The email is then used to pull the user from the database with the found email from the claim. If a user is found with an email, we abort the operation and return an authorized response to the client.

After successfully getting a user from the database using the email, we set the user into the context using the c.Set("user", user) and move the control to the next handler function using the c.Next() function.

Now we have the database, middleware and controller ready, we now pull together our code in the main.go file. This is the main entry point to our application.

main.go 

package main
 
import (
   "fmt"
   "net/http"
 
   "example.com/jwt-demo/controller"
   "example.com/jwt-demo/middleware"
   "example.com/jwt-demo/model"
   "github.com/gin-gonic/gin"
)
 
func init() {
   model.SetDBClient()
}
func main() {
   fmt.Println("Welcome to Go authorization with Go")
   r := gin.Default()
   r.GET("/", func(c *gin.Context) {
       c.JSON(http.StatusOK, gin.H{
           "message": "Home router",
       })
   })
 
   r.POST("/signup", controller.Signup)
   r.POST("/login", controller.Login)
   r.GET("/api/v1", middleware.Authorize, controller.Resources)
 
   r.Run(":5000")
}

In the main.go file, we initialize a gin router using the r := gin.Default() which returns a gin engine. We use the gin router to expose the POST and GET methods. The Signup and Login methods use the POST HTTP method and route requests to the controller.Signup and controller.Login handler respectively.

 

Testing 

To test the JWT token, we will use Postman to signup and login into our application.

We first need to run our application. In your terminal move to the root folder where main.go file is and run the below command

Example

$ go run main.go
Connection to the database is successful
Welcome to Go authorization with Go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)
 
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] POST   /signup                   --> example.com/jwt-demo/controller.Signup (3 handlers)
[GIN-debug] POST   /login                    --> example.com/jwt-demo/controller.Login (3 handlers)
[GIN-debug] GET    /api/v1                   --> example.com/jwt-demo/controller.Resources (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000

 

Signup user

GO JWT Authentication and Authorization Tutorial

 

Login User

GO JWT Authentication and Authorization Tutorial

 

Get resource with token

GO JWT Authentication and Authorization Tutorial

After making this request, if you clear your session and cookies, you will not be able to access data from the resources route.

 

Get resources without token

GO JWT Authentication and Authorization Tutorial

Summary

In this article, we learn about authentication and authorization using JWT. We learn about how to sign up a user, login a user and generate a token for the users and later on access resources only if a user has a token.

 

References

https://jwt.io
https://github.com/golang-jwt/jwt

 

Antony Shikubu

Antony Shikubu

He is highly skilled software developer with expertise in Python, Golang, and AWS cloud services. Skilled in building scalable solutions, he specializes in Django, Flask, Pandas, and NumPy for web apps and data processing, ensuring robust and maintainable code for diverse projects. You can reach out to him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment