How to Implement Authentication and Authorization in Golang.

How to Implement Authentication and Authorization in Golang.

In today's world, security is a critical aspect of any application and it's crucial to ensure that the application and its data are protected from unauthorized access. In this blog, we will dive into the implementation of Role-Based Access Control (RBAC) and Authentication in Golang, which are essential security mechanisms to secure your application and its data. We will explore how to implement RBAC and authentication in Golang and what steps are necessary to implement it effectively. So, grab a cup of coffee and get ready to learn about securing your application with RBAC and authentication in Golang!

Let's start reading... To INFINITY and beyond! - Buzz and Woody (Toy Story)  Meme | Make a Meme

🤔 Understanding JSON Web Token (JWT)

JWT, or JSON Web Token, is a widely used standard for authentication and authorization on the web. It is a compact and self-contained token that contains information about the authentication of a user. In our blog, we will be using JWT for authentication.

A JWT token typically consists of three parts:

  1. Header: The header defines the type of the token and the signing algorithm used.

  2. Payload: The payload contains the claims or information about the user, such as their name, ID, and roles.

  3. Signature: The signature is used to verify that the token has not been altered during transmission and is signed using a secret key known only to the server.

In summary, using JWT for authentication means that we will verify the user's identity by exchanging JWT tokens between the client and server and verifying the token's signature to ensure its authenticity.

🏁 Initializing the Project

Let's start with the project. Follow the below steps:

1️⃣ Creating project repository:

Let's name our repository go-auth.

mkdir go-auth
cd go-auth

2️⃣ Initializing the new go module:

The below command initializes a new Go module to track your code's dependencies.

go mod init go-auth

You will see a go.mod file created in the repository after this command.

3️⃣ Downloading Dependencies:

Now that we have a module to track our dependencies, let's quickly install all the dependencies necessary for our project.

We plan to utilize the following dependencies in our project:

  1. gin-gonic - A high-performance HTTP web framework for the Go programming language.

  2. gorm - An ORM library for Go that allows interaction with relational databases.

  3. postgres - A Go library for GORM PostgreSQL Driver

  4. jwt-go - A Go library for encoding and decoding JSON Web Tokens (JWT).

  5. godotenv - A Go library for managing environment variables from a .env file.

  6. bcrypt - A Go library that implements the bcrypt password hashing function.

go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/dgrijalva/jwt-go
go get github.com/joho/godotenv
go get golang.org/x/crypto/bcrypt

🗂️ Setting up server directories/packages

For the organization of our server, we will categorize the directories into the following sections:

  1. routes - This directory will contain the definitions of the different routes for the server, which map URLs to specific functions.

  2. controllers - This directory will hold the functions that will handle the incoming requests and interact with the models and other components.

  3. models - This directory will store the data models that represent the objects in the application and their relationships.

  4. middlewares - This directory will contain any middleware functions that process requests and responses, such as authentication or logging.

  5. utils - This directory will house various utility functions that are used throughout the application, such as helper functions or custom data types.

These directories will also serve as packages.

📦 Defining the Packages

1️⃣ Defining Models:

We will now proceed with setting up the models for our application.

  1. User - This model will represent a user of the application and will likely include fields such as username, email, and password.

     // PATH: go-auth/models/User.go
    
     package models
    
     import "gorm.io/gorm"
    
     type User struct {
         gorm.Model
         Name     string `json:"name"`
         Email    string `gorm:"unique" json:"email"`
         Password string `json:"password"`
         Role     string `json:"role"`
     }
    
  2. Claims - This model will contain information about the claims associated with a JSON Web Token (JWT). It will include the standard claims defined by the JWT specification, as well as a field for the user's role within the application.

     // PATH: go-auth/models/Claims.go
    
     package models
    
     import "github.com/dgrijalva/jwt-go"
    
     type Claims struct {
         Role string `json:"role"`
         jwt.StandardClaims
     }
    

2️⃣ Initializing Database:

Having our models ready, let's Initialize and set up a connection to a PostgreSQL database using the GORM library.

// PATH: go-auth/models/index.go

package models

import (
    "fmt"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type Config struct {
    Host     string
    Port     string
    User     string
    Password string
    DBName   string
    SSLMode  string
}

var DB *gorm.DB

func InitDB(cfg Config) {

    dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s", cfg.Host, cfg.User, cfg.Password, cfg.DBName, cfg.Port, cfg.SSLMode)

    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    if err := db.AutoMigrate(&User{}); err != nil {
        panic(err)
    }

    fmt.Println("Migrated database")

    DB = db
}

Let's understand the above code in detail:

  1. The Config struct holds the configuration values for the database connection, such as the host, port, user, password, database name, and SSL mode.

  2. The DB variable is a global variable that will store the instance of the database connection.

  3. The InitDB function is used to initialize the database connection using the values from the Config struct.

    • It generates the data source name (DSN) string from the configuration values.

    • It opens a connection to the PostgreSQL database using the GORM library and the generated DSN. If there is an error, it panics.

    • It automatically migrates the User model by calling AutoMigrate. This creates the necessary table in the database if it doesn't exist. If there is an error, it panics.

    • It sets the global DB variable to the instance of the database connection.

3️⃣ Creating Routes:

Now, let's define the routes for a web application using the Gin HTTP framework.

// PATH: go-auth/routes/auth.go

package routes

import (
    "go-auth/controllers"

    "github.com/gin-gonic/gin"
)

func AuthRoutes(r *gin.Engine) {
    r.POST("/login", controllers.Login)
    r.POST("/signup", controllers.Signup)
    r.GET("/home", controllers.Home)
    r.GET("/premium", controllers.Premium)
    r.GET("/logout", controllers.Logout)
}

This code defines the routes for the web application but does not handle the logic for each route. That logic is implemented in the controllers package.

Let's understand the above code in detail:

  1. The AuthRoutes function takes a gin.Engine instance as an argument and sets up the routes for the application.

  2. Using the r.POST method, a route for a login request is defined that maps to the Login function in the controllers package.

  3. Similarly, using the r.POST method, a route for a signup request is defined that maps to the Signup function in the controllers package.

  4. Using the r.GET method, a route for a home request is defined that maps to the Home function in the controllers package.

  5. Using the r.GET method, a route for a premium request is defined that maps to the Premium function in the controllers package.

  6. Using the r.GET method, a route for a logout request is defined that maps to the Logout function in the controllers package.

4️⃣ Defining main.go (The Driver Code):

This is the main driver code that will orchestrate the whole project.

// PATH: go-auth/main.go

package main

import (
    "go-auth/models"
    "go-auth/routes"
    "log"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
)

func main() {
    // Create a new gin instance
    r := gin.Default()

    // Load .env file and Create a new connection to the database
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    config := models.Config{
        Host:     os.Getenv("DB_HOST"),
        Port:     os.Getenv("DB_PORT"),
        User:     os.Getenv("DB_USER"),
        Password: os.Getenv("DB_PASSWORD"),
        DBName:   os.Getenv("DB_NAME"),
        SSLMode:  os.Getenv("DB_SSLMODE"),
    }

    // Initialize DB
    models.InitDB(config)

    // Load the routes
    routes.AuthRoutes(r)

    // Run the server
    r.Run(":8080")
}

This is the main function of the application. It performs the following actions:

  1. Creates a new instance of the Gin web framework.

  2. Loads the environment variables from a .env file.

  3. Initializes the database connection using the configurations obtained from the environment variables.

  4. Calls the AuthRoutes function to define the routes for the application.

  5. Starts the web server on port 8080.


Meme Time:

Tired? Refresh your mind with this Low Effort Meme:
*le gophers after reading this much.

Go pointers demystified


5️⃣ Defining Controllers:

This will contain all the logic for the authentication and authorization which was previously mapped in routes package.

// PATH: go-auth/controllers/auth.go

package controllers

import (
    "go-auth/models"
    "time"

    "go-auth/utils"

    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
)

// The string "my_secret_key" is just an example and should be replaced with a secret key of sufficient length and complexity in a real-world scenario.
var jwtKey = []byte("my_secret_key")

This code consists of several functions each handling different parts of the authentication process.

  • Login: Handles user login requests by receiving user credentials, verifying if the user exists, and checking if the provided password is correct. If everything is okay, it generates a JSON Web Token (JWT) and sets it as a cookie in the client's browser.

      // PATH: go-auth/controllers/auth.go
    
      func Login(c *gin.Context) {
    
          var user models.User
    
          if err := c.ShouldBindJSON(&user); err != nil {
              c.JSON(400, gin.H{"error": err.Error()})
              return
          }
    
          var existingUser models.User
    
          models.DB.Where("email = ?", user.Email).First(&existingUser)
    
          if existingUser.ID == 0 {
              c.JSON(400, gin.H{"error": "user does not exist"})
              return
          }
    
          errHash := utils.CompareHashPassword(user.Password, existingUser.Password)
    
          if !errHash {
              c.JSON(400, gin.H{"error": "invalid password"})
              return
          }
    
          expirationTime := time.Now().Add(5 * time.Minute)
    
          claims := &models.Claims{
              Role: existingUser.Role,
              StandardClaims: jwt.StandardClaims{
                  Subject:   existingUser.Email,
                  ExpiresAt: expirationTime.Unix(),
              },
          }
    
          token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    
          tokenString, err := token.SignedString(jwtKey)
    
          if err != nil {
              c.JSON(500, gin.H{"error": "could not generate token"})
              return
          }
    
          c.SetCookie("token", tokenString, int(expirationTime.Unix()), "/", "localhost", false, true)
          c.JSON(200, gin.H{"success": "user logged in"})
      }
    
  • Signup: Handles user signup requests by receiving user information, checking if the user already exists, hashing the password, and creating a new user in the database.

      // PATH: go-auth/controllers/auth.go
    
      func Signup(c *gin.Context) {
          var user models.User
    
          if err := c.ShouldBindJSON(&user); err != nil {
              c.JSON(400, gin.H{"error": err.Error()})
              return
          }
    
          var existingUser models.User
    
          models.DB.Where("email = ?", user.Email).First(&existingUser)
    
          if existingUser.ID != 0 {
              c.JSON(400, gin.H{"error": "user already exists"})
              return
          }
    
          var errHash error
          user.Password, errHash = utils.GenerateHashPassword(user.Password)
    
          if errHash != nil {
              c.JSON(500, gin.H{"error": "could not generate password hash"})
              return
          }
    
          models.DB.Create(&user)
    
          c.JSON(200, gin.H{"success": "user created"})
      }
    
  • Home: Handles requests to the home page by checking if the user is authorized to access it by looking for the JWT in the client's cookie and verifying it. If the user is authorized, the function returns the "home page" message along with the user's role.

      // PATH: go-auth/controllers/auth.go
    
      func Home(c *gin.Context) {
    
          cookie, err := c.Cookie("token")
    
          if err != nil {
              c.JSON(401, gin.H{"error": "unauthorized"})
              return
          }
    
          claims, err := utils.ParseToken(cookie)
    
          if err != nil {
              c.JSON(401, gin.H{"error": "unauthorized"})
              return
          }
    
          if claims.Role != "user" && claims.Role != "admin" {
              c.JSON(401, gin.H{"error": "unauthorized"})
              return
          }
    
          c.JSON(200, gin.H{"success": "home page", "role": claims.Role})
      }
    
  • Premium: Similar to the Home function but only returns the "premium page" message and the user's role if the user has an "admin" role.

      // PATH: go-auth/controllers/auth.go
    
      func Premium(c *gin.Context) {
    
          cookie, err := c.Cookie("token")
    
          if err != nil {
              c.JSON(401, gin.H{"error": "unauthorized"})
              return
          }
    
          claims, err := utils.ParseToken(cookie)
    
          if err != nil {
              c.JSON(401, gin.H{"error": "unauthorized"})
              return
          }
    
          if claims.Role != "admin" {
              c.JSON(401, gin.H{"error": "unauthorized"})
              return
          }
    
          c.JSON(200, gin.H{"success": "premium page", "role": claims.Role})
      }
    
  • Logout: Handles user logout requests by deleting the JWT cookie in the client's browser.

      // PATH: go-auth/controllers/auth.go
    
      func Logout(c *gin.Context) {
          c.SetCookie("token", "", -1, "/", "localhost", false, true)
          c.JSON(200, gin.H{"success": "user logged out"})
      }
    

The JWT is encrypted and signed using the HS256 algorithm and the secret key jwtKey.

REMEMBER: The string "my_secret_key" is just an example and should be replaced with a secret key of sufficient length and complexity in a real-world scenario.

6️⃣ Creating Utility Function:

We are going to need these 3 utility functions in our project.

  1. GenerateHashPassword: This function takes a plain text password as input and returns a hash value generated from it using a one-way hashing algorithm. The purpose of this function is to store a user's password securely in the database. This way, even if the database is compromised, the attacker cannot retrieve the original password as it is encrypted.

     // PATH: go-auth/utils/GenerateHashPassword.go
    
     package utils
    
     import "golang.org/x/crypto/bcrypt"
    
     func GenerateHashPassword(password string) (string, error) {
         bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
         return string(bytes), err
     }
    
  2. CompareHashPassword: This function takes the user input password and the hashed password stored in the database and compares them. If the hashes match, it returns true. This function is used to verify if the user has entered the correct password during login.

     // PATH: go-auth/utils/CompareHashPassword.go
    
     package utils
    
     import "golang.org/x/crypto/bcrypt"
    
     func CompareHashPassword(password, hash string) bool {
         err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
         return err == nil
     }
    
  3. ParseToken: This function takes a JWT token as input and returns the claims contained in it. Claims are a set of key-value pairs that represent the information being transmitted between parties. In this case, the claims may include the subject (email), role, and expiration time of the token. This function is used to validate if the token is legitimate and to retrieve information contained in it.

     // PATH: go-auth/utils/ParseToken.go
    
     package utils
    
     import (
         "go-auth/models"
    
         "github.com/dgrijalva/jwt-go"
     )
    
     func ParseToken(tokenString string) (claims *models.Claims, err error) {
         token, err := jwt.ParseWithClaims(tokenString, &models.Claims{}, func(token *jwt.Token) (interface{}, error) {
             return []byte("my_secret_key"), nil
         })
    
         if err != nil {
             return nil, err
         }
    
         claims, ok := token.Claims.(*models.Claims)
    
         if !ok {
             return nil, err
         }
    
         return claims, nil
     }
    

7️⃣ Creating Middlewares:

This is an example of middleware for authorization. You can use this to authorize any route you create.

// PATH: go-auth/middlewares/isAuthorized.go

package middlewares

import (
    "go-auth/utils"

    "github.com/gin-gonic/gin"
)

func IsAuthorized() gin.HandlerFunc {
    return func(c *gin.Context) {
        cookie, err := c.Cookie("token")

        if err != nil {
            c.JSON(401, gin.H{"error": "unauthorized"})
            c.Abort()
            return
        }

        claims, err := utils.ParseToken(cookie)

        if err != nil {
            c.JSON(401, gin.H{"error": "unauthorized"})
            c.Abort()
            return
        }

        c.Set("role", claims.Role)
        c.Next()
    }
}

The function IsAuthorized returns a Gin handler function, which is used to process HTTP requests and responses in a Gin web application.

The handler function retrieves the "token" cookie from the incoming HTTP request and tries to parse it using the ParseToken function from the utils package. If the cookie is missing or the parsing fails, the function returns a JSON response with a 401 Unauthorized HTTP status code and an error message, indicating that the user is not authorized to access the requested resource.

If the token parsing is successful, the function sets the "role" value in the context object (c) to the value of the "Role" claim in the parsed token, and calls the Next method to continue processing the request in the next middleware or handler in the middleware chain.

😇 Conclusion

In conclusion, implementing authentication and authorization in Golang is a straightforward process. By following the steps outlined in this article, you can create a secure and efficient authentication and authorization system for your Golang application. Whether you are building a simple web app or a complex enterprise system, Golang provides all the tools you need to ensure that your users' data is protected. By using JWT tokens and hashing techniques, you can ensure that your authentication and authorization system is reliable, scalable, and easy to maintain. With the right tools and knowledge, you can make your Golang application a secure and trusted platform for your users.

GitHub Repository of the Project: https://github.com/tanmayVaish/go-auth