DEV Community

Carlos Armando Marcano Vargas
Carlos Armando Marcano Vargas

Posted on • Originally published at carlosmv.hashnode.dev on

JWT Authentication with Gin | Go

In this article, we will build an authentication API with two endpoints, a register endpoint to sign up users and a login endpoint. Then we will add two endpoints that will need authentication and authorization to get access. For this article will be an endpoint to get a list of groceries and an endpoint to add a grocery to the database.

We will use the Gin framework and GORM as ORM.

Prerequisites:

  • Go basic knowledge
  • JWT knowledge
  • Gin
  • CRUD operations with GORM

This will be our directory's structure:

handlers/
          auth.go
          grocery.go
models/
        database.go
        user.go
        grocery.go
middleware/
        jwtMiddleware.go
utils/
      token.go
main.go
.env

Enter fullscreen mode Exit fullscreen mode

Briefly, I will explain these packages. In the models module, we have the setup for our database, the model for users with functions to verify and hash the password, and the model for groceries. Every user will be able to add groceries and retrieve all the groceries added.

In the handlers module, we have the handlers for registering and login users. Handlers for retrieving and adding groceries.

In the middleware module, we have the jwtMiddleware to handle incoming requests and ensure the users are authenticated.

We place all the functions related to generating JWT tokens, getting tokens, and parsing JWT in utils.

Setup

First, let's create our module:

go mod init <your module name>

Enter fullscreen mode Exit fullscreen mode

Then we install the packages we are going to use:

go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u github.com/golang-jwt/jwt/v4
go get -u github.com/joho/godotenv
go get -u golang.org/x/crypto

Enter fullscreen mode Exit fullscreen mode

In our root directory, we create a package for our model and file to set up our database:

user.go

package models

import (

    "gorm.io/gorm"

)

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"-"`
}

Enter fullscreen mode Exit fullscreen mode

In this file, we create a User struct for our model with two fields: Username and Password. But also gorm.Model adds the fields: ID, CreatedAt, UpdatedAt, and DeletedAt.

databaseSetup.go

package models

import (
    "fmt"

    "github.com/joho/godotenv"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "log"
    "os"
)

func Setup() (*gorm.DB, error) {

    err := godotenv.Load()
    if err != nil {
        log.Println("Error loading .env file")
    }
    dbUrl := fmt.Sprint(os.Getenv("DATABASE_URL"))

    db, err := gorm.Open(sqlite.Open(dbUrl), &gorm.Config{})

    if err != nil {
        log.Fatal(err.Error())
    }
    if err = db.AutoMigrate(&User{}); err != nil {
        log.Println(err)
    }

    return db, err
}

Enter fullscreen mode Exit fullscreen mode

In this file we create a database using SQLite, we pass the database URL to sqlite.Open().

Register Handler

models/user.go

package models

import (
    "errors"

    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
    "html"
    "log"
    "strings"
)

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"-"`
}

func (user *User) HashPassword() error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)

    if err != nil {
        return err
    }
    user.Password = string(hashedPassword)

    user.Username = html.EscapeString(strings.TrimSpace(user.Username))

    return nil
}

Enter fullscreen mode Exit fullscreen mode

In the user.go file we add the function to hash the password using bcrypt library.

auth.go

package handlers

import (
    "github.com/carlosm27/jwtGinApi/models"
    "github.com/carlosm27/jwtGinApi/utils"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
    "net/http"
)

type RegisterInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type LoginInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type Server struct {
    db *gorm.DB
}

func NewServer(db *gorm.DB) *Server {
    return &Server{db: db}
}

func (s *Server) Register(c *gin.Context) {
    var input RegisterInput

    if err := c.ShouldBind(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user := models.User{Username: input.Username, Password: input.Password}
    user.HashPassword()

    if err := s.db.Create(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    }

    c.JSON(http.StatusCreated , gin.H{"message": "User created"})
}

func (s *Server) Login(c *gin.Context) {

}

Enter fullscreen mode Exit fullscreen mode

Here, we develop our register and login handlers. The login handler will be developed further in this article.

The register handler takes the username and password passed by the user and binds them to the variable input, if any or some field is missed, it will send a BadRequest status code.

Then, the username and password are passed to the user variable to create a User instance. Then we pass the variable user as a reference to the create() function after we hash the password, to create a user in our database.

If there is no problem creating a user, the server will send the message "User created".

main.go

package main

import (
    "github.com/carlosm27/jwtGinApi/models"
    "github.com/carlosm27/jwtGinApi/handlers"
    "github.com/carlosm27/jwtGinApi/middleware"
    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    "gorm.io/gorm"
    "log"
    "os"
)

func DbInit() *gorm.DB {
    db, err := models.Setup()
    if err != nil {
        log.Println("Problem setting up database")
    }
    return db
}

func SetupRouter() *gin.Engine {
    r := gin.Default()

    db := DbInit()

    server := handlers.NewServer(db)

    router := r.Group("/api")

    router.POST("/register", server.Register)
    router.POST("/login", server.Login)

    return r

}

Enter fullscreen mode Exit fullscreen mode

In main.go we create a function to initialize our database and create our routers. We create an instance of our database and pass it as an argument to initialize an instance of the server and call our handlers.

func main() {

    if err := godotenv.Load(); err != nil {
        log.Println("Error loading .env file")
    }
    port := os.Getenv("PORT")

    r := SetupRouter()

    log.Fatal(r.Run(":"+port))

}

Enter fullscreen mode Exit fullscreen mode

Here we load our environment variables and initialize the Gin router.

go run github.com/<username>/<module>

Enter fullscreen mode Exit fullscreen mode

image.png

Login handler

To develop our login handler we need to write a couple of functions to check the login credentials and verify the password.

user.go

 ...

func VerifyPassword(password, hashedPassword string) error {
    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

Enter fullscreen mode Exit fullscreen mode

The VerifyPassword function verifies if the password passed by the user matches the hashed password. This function uses The compareHashAndPassword function from the bcrypt library.

If the password and the hashed password match, then the function continues and generate a token, and returns it.

We create a folder named "utils" in our root directory and create the token.go file.

token.go

package utils

import (
    "fmt"
    "github.com/carlosm27/jwtGinApi/models"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v4"
    "os"
    "strconv"
    "strings"
    "time"
)

func GenerateToken(user model.User) (string, error) {

    tokenLifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))

    if err != nil {
        return "", err
    }

    claims := jwt.MapClaims{}
    claims["authorized"] = true
    claims["id"] = user.ID
    claims["exp"] = time.Now().Add(time.Hour * time.Duration(tokenLifespan)).Unix()
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    return token.SignedString([]byte(os.Getenv("API_SECRET")))

}

Enter fullscreen mode Exit fullscreen mode

According to its documentation, MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. This is the default claims type if you don't supply one.

Then, we passed SigningMethodHS256 and the variable claims to the function NewWithClaims. This function creates a new Token with the specified signing method and claims, according to the documentation.

And finally return a SignedString that creates and returns a complete, signed JWT. The token is signed using the SigningMethod specified in the token. We passed to this function an "API_SECRET" or a "SECRETE_KEY".

To generate a key, we have to have installed OpenSSL in our machine, and execute the following command in our terminal.

openssl rand -hex 32

Enter fullscreen mode Exit fullscreen mode

And the output will be something like this:

09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7

Enter fullscreen mode Exit fullscreen mode

auth.go

func (s *Server)LoginCheck(username, password string) (string, error) {
    var err error

    user := models.User{}

    if err = s.db.Model(models.User{}).Where("username=?", username).Take(&user).Error; err != nil {
        return "", err
    }

    err = models.VerifyPassword(password, user.Password)

    if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
        return "", err
    }

    token, err := utils.GenerateToken(user)

    if err != nil {
        return "", err
    }

    return token, nil

}

Enter fullscreen mode Exit fullscreen mode

In the LoginCheck function we pass a username and a password. The function search in the database to see if the username exists. If it exists, then the function proceeds to verify the password.

...

func (s *Server) Login(c *gin.Context) {
    var input LoginInput

    if err := c.ShouldBind(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user := models.User{Username: input.Username, Password: input.Password}

    token, err := models.LoginCheck(user.Username, user.Password)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "The username or password is not correct"})
        return
    }

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

Enter fullscreen mode Exit fullscreen mode

The Login function receives a username and a password and binds them to input for data validation.

Then it creates a User instance with the data passed and checks the credentials. If the credentials are valid, then it returns a token.

go run github.com/<username>/<module>

Enter fullscreen mode Exit fullscreen mode

image.png

Authentication

token.go

func ValidateToken (c *gin.Context) error {
    token, err := GetToken(c)

    if err != nil {
        return err
    }

    _, ok := token.Claims.(jwt.MapClaims)
    if ok && token.Valid {
        return nil
    }

    return errors.New("Invalid token provided")
}

Enter fullscreen mode Exit fullscreen mode

ValidateToken verify that incoming requests contain a valid token. If there is not a valid token, it will return an error.

func GetToken(c *gin.Context) (*jwt.Token, error) {
    tokenString := getTokenFromRequest(c)
    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["alg"])
        }

        return privateKey, nil
    })
    return token, err
}

Enter fullscreen mode Exit fullscreen mode

GetToken receives a token and parses it.

The documentation says that the method Parse parses, validates, verifies the signature, and returns the parsed token. keyFunc will receive the parsed token and should return the key for validation.

type Keyfunc func(*Token) (interface{}, error)

Enter fullscreen mode Exit fullscreen mode

Keyfunc will be used by the Parse methods as a callback function to supply the key for verification. The function receives the parsed, but unverified Token. This allows you to use properties in the Header of the token (such as kid) to identify which key to use.

func getTokenFromRequest(c *gin.Context) string {
    bearerToken := c.Request.Header.Get("Authorization")

    splitToken := strings.Split(bearerToken, " ")
    if len(splitToken) == 2 {
        return splitToken[1]
    }
    return ""
}

Enter fullscreen mode Exit fullscreen mode

Bearer tokens come in the format bearer <JWT>, so we split them to return a JWT string.

Authorization

middleware.go

package middleware

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

func JwtAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {

        err := utils.ValidateToken(c)

        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"Unauthorized":"Authentication required"})
            fmt.Println(err)
            c.Abort()
            return
        }
        c.Next()
    }
}

Enter fullscreen mode Exit fullscreen mode

We create a custom middleware. This handler receives a token and calls ValidateToken(), if the token is valid, the next handler is called. Otherwise, it will return the status code Unauthorized.

models/grocery.go

package models

import (

    "gorm.io/gorm"

)

type Grocery struct {
    gorm.Model
    Name string `json: "name"`
    Quantity int `json: "quantity"`

}

Enter fullscreen mode Exit fullscreen mode

database.go

package models

import (
    "fmt"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "log"
    "os"
)

func Setup() (*gorm.DB, error) {

    dbUrl := fmt.Sprint(os.Getenv("DATABASE_URL"))

    db, err := gorm.Open(sqlite.Open(dbUrl), &gorm.Config{})

    if err != nil {
        log.Fatal(err.Error())
    }
    if err = db.AutoMigrate(&User{}); err != nil {
        log.Println(err)
    }

    if err = db.AutoMigrate(&Grocery{}); err != nil {
        log.Println(err)
    }

    return db, err
}

Enter fullscreen mode Exit fullscreen mode

handlers/grocery.go

package handlers

import (
    "github.com/carlosm27/jwtGinApi/models"
    "github.com/gin-gonic/gin"
    "net/http"

)

type NewGrocery struct {
    Name string `json: "name" binding: "required"`
    Quantity int `json: "quantity" binding: "required"`
}

func (s *Server) GetGroceries(c *gin.Context) {

    var groceries []models.Grocery

    if err := s.db.Find(&groceries).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, groceries)

}

func (s *Server) PostGrocery(c *gin.Context) {

    var grocery NewGrocery

    if err := c.ShouldBindJSON(&grocery); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    newGrocery := models.Grocery{Name: grocery.Name, Quantity: grocery.Quantity}

    if err := s.db.Create(&newGrocery).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, newGrocery)
}

Enter fullscreen mode Exit fullscreen mode

In this file are two handlers. One is to get all the groceries in the database. And the other is to add a grocery to the database.

These two handlers will be accessed only by authorized users.

main.go

package main

import (
    "github.com/carlosm27/jwtGinApi/models"
    "github.com/carlosm27/jwtGinApi/handlers"
    "github.com/carlosm27/jwtGinApi/middleware"
    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    "gorm.io/gorm"
    "log"
    "os"
)

func DbInit() *gorm.DB {
    db, err := models.Setup()
    if err != nil {
        log.Println("Problem setting up database")
    }
    return db
}

func SetupRouter() *gin.Engine {
    r := gin.Default()

    db := DbInit()

    server := handlers.NewServer(db)

    router := r.Group("/api")

    router.POST("/register", server.Register)
    router.POST("/login", server.Login)

    authorized := r.Group("/api/admin")
    authorized.Use(middleware.JwtAuthMiddleware())
    authorized.GET("/groceries", server.GetGroceries)
    authorized.POST("/grocery", server.PostGrocery)

    return r

}

Enter fullscreen mode Exit fullscreen mode

We create a router group for our groceries handlers with the JwtAuthMiddleware handler. This ensures that anyone who wants to get access to GetGroceries and PostGrocery has to be authorized and pass through the middleware.

With this approach, any authorized users can see all the groceries in the database, even the groceries add it by other users.

Now, we will add a one-to-many relationship. So, every user only can add and get only their groceries.

Adding one-to-many relationship.

models/user.go

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"-"`
    Groceries []Grocery
}

func GetUserById(uid uint) (User, error) {
    var user User

    db, err := Setup()

    if err != nil {
        log.Println(err)
        return User{}, err
    }
    if err := db.Preload("Groceries").Where("id=?", uid).Find(&user).Error; err != nil {
        return user, errors.New("user not found")

    }

    return user, nil
}

Enter fullscreen mode Exit fullscreen mode

We add the field Grocery to the User. And we add the GetUserById function. This function will receive an uid and will search in the database for the user with that uid.

models/grocery.go

package models

import (

    "gorm.io/gorm"
)

type Grocery struct {
    gorm.Model
    Name string `json: "name"`
    Quantity int `json: "quantity"`
    UserId uint
}

Enter fullscreen mode Exit fullscreen mode

Here we add the field UserId, which represents the owner of the grocery.

utils/token.go

...

func CurrentUser(c *gin.Context) (models.User, error) {
    err := ValidateToken(c)
    if err != nil {
        return models.User{}, err
    }
    token, _ := GetToken(c)
    claims, _ := token.Claims.(jwt.MapClaims)
    userId := uint(claims["id"].(float64))

    user, err := models.GetUserById(userId)
    if err != nil {
        return models.User{}, err
    }
    return user, nil
}

Enter fullscreen mode Exit fullscreen mode

CurrentUser extract the token from the request and validate it, and then extract the "id" of the user from the claims. Then the "id" is passed to the GetUserById function and returns the user.

handlers/grocery.go

func (s *Server) GetGroceries(c *gin.Context) {

    user, err := utils.CurrentUser(c)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"data": user.Groceries})

}

Enter fullscreen mode Exit fullscreen mode

Now GetGroceries only will return the list of the groceries of the user who is making the request.

image.png

func (s *Server) PostGrocery(c *gin.Context) {

    var grocery models.Grocery

    if err := c.ShouldBindJSON(&grocery); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user, err := utils.CurrentUser(c)
    if err != nil {

        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    grocery.UserId = user.ID

    if err := s.db.Create(&grocery).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusCreated, grocery)
}

Enter fullscreen mode Exit fullscreen mode

When a user wants to add a grocery, only will be added to the list the user owns.

image.png

Conclusion

This is the first time I build an authentication app using JWT, and writing this article help me a lot to understand JWT. It was a relief to find enough resources about JWT and how to build an authentication app with Go like the article written by Oluyemi Olususi or the article written by Seef Nasrul, because I was so confused about the pattern, and how the server knows that is an authenticated user who is trying to fetch the data. And those articles help me a lot and the Jwt-Go documentation of course.

You don't have to build your own authentication for every app you build, you may use any alternative of BaaS. But, I did it because I wanted to learn about it. And I learn by building and writing.

I hope you enjoy this article, thank you for taking the time to read it.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, or LinkedIn.

The source code is here

References

Top comments (1)

Collapse
 
krifoelectron profile image
Arsenii

is it right to do logout like this?

package handlers

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

func (s *Server) Logout(context *gin.Context) {
context.SetCookie("Mental", "", -1, "", "", false, true)
}

or it is possible to ban the token