JWT Authentication in Go

JWT Authentication in Go

REST-API with JWT Authentication in Go using Fiber, PostgreSQL and GORM

Introduction :

Hey there 👋, In this tutorial, we are going to learn about implementing JWT Authentication in Golang REST-APIs using Fiber Web Framework, PostgreSQL DB and GORM.

JWT :

JSON Web Token (JWT) is a JSON-based open standard (RFC 7519) for creating access tokens that assert some number of claims. For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that it is logged in as admin. The tokens are signed by the server's key, so the client and server are both able to verify that the token is legitimate. This allows the server to trust the token and grants the client the permissions associated with that token.

JWTs are commonly used as a way to authenticate users. When a user logs in to a server, the server creates a JWT that contains information about the user and signs it using a secret key. The server then sends the token back to the client, and the client stores it for future use. When the client wants to access a protected route or resource, it sends the JWT along with the request. The server can then verify the token and grant access to the protected route or resource if the token is valid.

What are we building :

We are going to build a Web API for a User to login, Register, See active User and Logout.

Prerequisites💯 :

To continue with the tutorial, firstly you need to have Golang, Fiber and PostgreSQL installed. If you've not gone through the previous tutorials on the Fiber Web Framework series you can see them here :)

Installations :

Getting Started 🚀:

Let's get started by creating the main project directory jwt-auth-api by using the following command.

(🟥Be careful, sometimes I've done the explanation by commenting in the code)

mkdir jwt-auth-api //Creates a 'jwt-auth-api' directory
cd jwt-auth-api //Change directory to 'jwt-auth-api'

Now initialize a mod file. (If you publish a module, this must be a path from which your module can be downloaded by Go tools. That would be your code's repository.)

go mod init <repository-name>

In my case repository name is github.com/Siddheshk02/jwt-auth-api .

To install the Fiber Framework run the following command :

go get -u github.com/gofiber/fiber/v2

To install the Gorm and to install the Gorm Postgres driver, run the following commands resp. :

go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

Initializing 💻:

Let's set up our server by creating a new instance of Fiber. For this create a file main.go and add the following code to it :

package main

import (
    "github.com/Siddheshk02/jwt-auth-api/routes" // importing the routes package 
    "github.com/gofiber/fiber/v2"
)

func main() {
    app := fiber.New()

    routes.Setup(app) // A routes package/folder is created with 'Setup' function created.

    app.Listen(":8000")
}

In the routes.go , all the endpoints are created for getting user, login, register and logout.

Routes :

Add the following code in the routes.go file :

package routes

import (
    "github.com/gofiber/fiber/v2"
)

func Setup(app *fiber.App) {
    api := app.Group("/user")

    api.Get("/get-user", func(c *fiber.Ctx) error {
        return c.SendString("Hello World!!")
    })
}

Now let's run and test the API for the GET endpoint.

Run go run main.go .

For testing the API, I'm using POSTMAN you can use any tool.

Now, let's add the other endpoints i.e. for login, register and logout.

The routes.go will look like the following code :

package routes

import (
    "github.com/Siddheshk02/jwt-auth-api/controllers" // importing the routes package 
    "github.com/gofiber/fiber/v2"
)

func Setup(app *fiber.App) {
    api := app.Group("/user")

    api.Get("/get-user", controllers.User)

    api.Post("/register", controllers.Register)

    api.Post("/login", controllers.Login)

    api.Post("/logout", controllers.Logout)
}

The User, Register, Login and Logout are the functions we are going to create in the controller.go file in the controller package/folder. These are going to perform the actual task.

Firstly, let's try a simple task through the Register function, the following code is added to the Register function.

(For now, You can comment on the other API endpoints i.e. /login , /get-user and /logout )

package controllers

import "github.com/gofiber/fiber/v2"

func Register(c *fiber.Ctx) error {
    var data map[string]string

    if err := c.BodyParser(&data); err != nil {
        return err
    }

    return c.JSON(data)

}

Now, test the /register endpoint by passing the data as shown in the image below.

If there is no error then the exact data will be printed.

Database :

Let's create the database and name it as jwt-auth-api , the steps to create the database are Database>>Create>>Database.

Once the database is created, now make a folder database in which we are going to make dbconn.go file. In this file, we are going to add the database connection and migration function.

package database

import (
    "fmt"
    "log"

    "github.com/Siddheshk02/jwt-auth-api/models" // this will be imported after you've created the User Model in the models.go file
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

const (
    host     = "localhost"
    port     = 5432
    user     = "postgres"
    password = "<password>" //Enter your password for the DB
    dbname   = "jwt-auth-api"
)

var dsn string = fmt.Sprintf("host=%s port=%d user=%s "+
    "password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
    host, port, user, password, dbname)

var DB *gorm.DB

func DBconn() {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }
    DB = db

    db.AutoMigrate(&models.User{}) // we are going to create a models.go file for the User Model.
}

The function gorm.Open() creates a new connection pool whenever it is called.

db.AutoMigrate() call helps in creating the table if it is not already present. Database migration is usually things that change the structure of the database over time and this helps in making sure that the database structure is properly migrated to the latest version.

Make a folder models in which models.go will be created and updated file with the following code in it.

package models

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Name     string `json:"name"`
    Email    string `json:"email" gorm:"unique"`
    Password []byte `json:"-"`
}

As you can see, our User Model will have a Name, Email, and Password. Here, the Email will be unique. This means, that once we complete our application and try to register new users with the same email, the code won’t allow us to do it. The best part is that you don’t have to write any code specifically for this. Everything is handled by GORM.

The gorm.Model specification adds some default properties to the Model, like id, created date, modified date, and deleted date.

Now, Update the main.go file with the following code.

package main

import (
    "github.com/Siddheshk02/jwt-auth-api/database"
    "github.com/Siddheshk02/jwt-auth-api/routes"
    "github.com/gofiber/fiber/v2"
)

func main() {
    database.DBconn()

    app := fiber.New()

    routes.Setup(app)

    app.Listen(":8000")
}

Register :

Let's update the register() function in the controllers.go file according to the User Model and database table we've created.

func Register(c *fiber.Ctx) error {
    var data map[string]string

    if err := c.BodyParser(&data); err != nil {
        return err
    }

    password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14) //GenerateFromPassword returns the bcrypt hash of the password at the given cost i.e. (14 in our case).

    user := models.User{
        Name:     data["name"],
        Email:    data["email"],
        Password: password,
    }

    database.DB.Create(&user) //Adds the data to the DB

    return c.JSON(user)

}

Now, test the endpoint to store the information.

Login :

Let's make the Login work. Before this uncomment the route for the login in the routes.go file

In the Login we will get the same data string as in Register and we will check the email entered with the database if it is present or not.

If it is present then we will compare the Passwords using an inbuilt function.

func Login(c *fiber.Ctx) error {
    var data map[string]string

    if err := c.BodyParser(&data); err != nil {
        return err
    }

    var user models.User

    database.DB.Where("email = ?", data["email"]).First(&user) //Check the email is present in the DB

    if user.ID == 0 { //If the ID return is '0' then there is no such email present in the DB
        c.Status(fiber.StatusNotFound)
        return c.JSON(fiber.Map{
            "message": "user not found",
        })
    }

    if err := bcrypt.CompareHashAndPassword(user.Password, []byte(data["password"])); err != nil {
        c.Status(fiber.StatusBadRequest)
        return c.JSON(fiber.Map{
            "message": "incorrect password",
        })
    } // If the email is present in the DB then compare the Passwords and if incorrect password then return error.

    return c.JSON(user) // If Login is Successfully done return the User data.

}

Test the Login endpoint :

Now we are successfully returning a user but we need to return a JWT Token.

For this, we need a package to be installed.

go get github.com/golang-jwt/jwt

Update the Login() function wit the following code,

const SecretKey = "secret"

func Login(c *fiber.Ctx) error {
    var data map[string]string

    if err := c.BodyParser(&data); err != nil {
        return err
    }

    var user models.User

    database.DB.Where("email = ?", data["email"]).First(&user) //Check the email is present in the DB

    if user.ID == 0 { //If the ID return is '0' then there is no such email present in the DB
        c.Status(fiber.StatusNotFound)
        return c.JSON(fiber.Map{
            "message": "user not found",
        })
    }

    if err := bcrypt.CompareHashAndPassword(user.Password, []byte(data["password"])); err != nil {
        c.Status(fiber.StatusBadRequest)
        return c.JSON(fiber.Map{
            "message": "incorrect password",
        })
    } // If the email is present in the DB then compare the Passwords and if incorrect password then return error.

    claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
        Issuer:    strconv.Itoa(int(user.ID)), //issuer contains the ID of the user.
        ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), //Adds time to the token i.e. 24 hours.
    })

    token, err := claims.SignedString([]byte(SecretKey))

    if err != nil {
        c.Status(fiber.StatusInternalServerError)
        return c.JSON(fiber.Map{
            "message": "could not login",
        })
    }

    cookie := fiber.Cookie{
        Name:     "jwt",
        Value:    token,
        Expires:  time.Now().Add(time.Hour * 24),
        HTTPOnly: true,
    } //Creates the cookie to be passed.

    c.Cookie(&cookie)

    return c.JSON(fiber.Map{
        "message": "success",
    })
}

NewWithClaims takes two parameters, a signing method and claims. Claims are the actual data that the JWT token will contain.

jwt.NewWithClaims doesn't create the new token, you need to call the SignedString function passing it the secret key to get the actual JWT token. We stored this token in a cookie.

Now, for cors issue i.e. (the problem which arises when the backend is running on a different port while the front-end is running on a different port), we are using the cors.New() function in the main.go file.

Update the main.go file, it will look like the following code

package main

import (
    "github.com/Siddheshk02/jwt-auth-api/database"
    "github.com/Siddheshk02/jwt-auth-api/routes"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
)

func main() {
    database.DBconn()

    app := fiber.New()

    app.Use(cors.New(cors.Config{
        AllowCredentials: true, //Very important while using a HTTPonly Cookie, frontend can easily get and return back the cookie.
    }))

    routes.Setup(app)

    app.Listen(":8000")
}

Now, let's test the login endpoint and see the response. Send the request and then click on the Cookies(1) section.

User :

Before moving ahead, uncomment the Get endpoint for the get-user in the routes.go file i.e. api.Get("/get-user", controllers.User) .

Now, let's update the function User() for getting the logged-in user by using the cookie.

func User(c *fiber.Ctx) error {
    cookie := c.Cookies("jwt")

    token, err := jwt.ParseWithClaims(cookie, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(SecretKey), nil //using the SecretKey which was generated in th Login function
    })

    if err != nil {
        c.Status(fiber.StatusUnauthorized)
        return c.JSON(fiber.Map{
            "message": "unauthenticated",
        })
    }

    claims := token.Claims.(*jwt.StandardClaims)

    var user models.User

    database.DB.Where("id = ?", claims.Issuer).First(&user)

    return c.JSON(user)

}

The jwt.ParseWithClaims accepts the secret key, it takes a function as the 3rd argument in which you have to return the key.

Test the get-user endpoint.

Here, we can see the password. So, if you don't want to show the password, update the models.go as,

//Previous : Password []byte `json:"password"`
Password []byte `json:"-"`

Send a request again and you'll not get the password.

Logout :

Before moving ahead, uncomment the Post endpoint for the logout in the routes.go file i.e. api.Post("/logout", controllers.Logout) .

Now, let's update the function Logout() for logging out the present user.

For logging out the user, we need to delete the cookie, but there is no way to remove the cookie in the browser. So, we'll create a different cookie and set its expiry time in the past. Then we'll set the cookie and return the response.

func Logout(c *fiber.Ctx) error {
    cookie := fiber.Cookie{
        Name:     "jwt",
        Value:    "",
        Expires:  time.Now().Add(-time.Hour), //Sets the expiry time an hour ago in the past.
        HTTPOnly: true,
    }

    c.Cookie(&cookie)

    return c.JSON(fiber.Map{
        "message": "success",
    })

}

Now, test the logout endpoint.

Now if you test the /get-user endpoint to see the logged-in user, you'll not be able to see any user.

So, there is no logged-in user. If we log-in again, then another cookie will be generated while retrieving the user.

So, this is how JWT Authentication works in golang.

The complete code is saved in this GitHub repository.

Conclusion :

You've Successfully created a REST-API and secured it with JWT Authentication ✨💯

To get more information about Golang concepts, projects, etc. and to stay updated on the Tutorials do follow Siddhesh on Twitter and GitHub.

Until then Keep Learning, Keep Building 🚀🚀

Â