DEV Community

Cover image for Build user authentication in Golang with JWT and mongoDB
JOOJO DONTOH
JOOJO DONTOH

Posted on • Updated on

Build user authentication in Golang with JWT and mongoDB

This article assumes that you already have Golang installed and you are aware of how to setup a simple golang app. If not don't worry the article will teach you how to do that.
This article also assumes that you have a working mongodb database available for this tutorial if not refer to the first section of this article to get one

Let's say you wanted to build a secure Restful API only available to your user base or registered users, how would you achieve that? πŸ€” This article is going to teach you just how to do that in Golang. The idea is mostly the same in other languages so once you grasp the concept it shouldn't be too hard to implement anywhere.

General concept

  1. First the user logs in from a client device which makes an API call to the server with the user's details
  2. The server verifies these details from the user to make sure the user exists in the system
  3. Once verification is complete, the server sends a secure token to client device.
  4. The received token can now be used by the client to grant access to api resources for the user.

Alt Text

Development Overview

  1. Setup.
    • Application setup
    • Database setup
  2. Create a User module.
    • User model
  3. Install and implement the bcrypt package.
    • Password hashing
    • Password verification upon login
    • Sign up service
    • Login Service
  4. Install and implement the JWT-GO package.
    • build token generator
    • build token validator
    • update token upon login
  5. Implement JWT middleware for validation.
  6. Setup routes.
  7. Write main.go file.

Let's begin!

This is the file structure you'll be using

Alt Text

Setting up the application

$ go build
$ ./new
Enter fullscreen mode Exit fullscreen mode

go build compiles the application. ./new is used to run the compiled file. This file will be located in your root folder and may have another name. I simply named mine new.

  • Create a folder and name it whatever you please. I'm gonna name mine user-athentication-golang. This is going to be your root folder.

  • My prefered IDE is VS code. Open the terminal in VScode and run go mod init. This will automatically create your go.mod file (a file that stores all the required packages for your app) and save it in your root folder.

  • Create a .env file (a file for storing your environmental variables) and fill it with the following.

PORT=8000
MONGODB_URL={{insert the db link here}}
Enter fullscreen mode Exit fullscreen mode
  • Create a folder (package) named "database". This folder will contain a file which manages your database connection and opens your collections. Create the file and name "databaseConnection.go" and fill it with the following code.
package database

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

//DBinstance func
func DBinstance() *mongo.Client {
    err := godotenv.Load(".env")

    if err != nil {
        log.Fatal("Error loading .env file")
    }

    MongoDb := os.Getenv("MONGODB_URL")

    client, err := mongo.NewClient(options.Client().ApplyURI(MongoDb))
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

    defer cancel()
    err = client.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to MongoDB!")

    return client
}

//Client Database instance
var Client *mongo.Client = DBinstance()

//OpenCollection is a  function makes a connection with a collection in the database
func OpenCollection(client *mongo.Client, collectionName string) *mongo.Collection {

    var collection *mongo.Collection = client.Database("cluster0").Collection(collectionName)

    return collection
}

Enter fullscreen mode Exit fullscreen mode
  • Create a Main.go file in your root folder and leave it empty for now

You are going to start by creating the user model. The user model is basically the structure of user details for every user in your system. Every user must at least have a

  • user ID (a unique string that identifies users)
  • first name
  • last name
  • email
  • password
  • token (the signed jwt token with the user details)
  • refresh token (an empty token for simply refreshing a page)

Follow the file structure as shown above and insert the following code into your models/userModel.go file

package models

import (
    "time"

    "go.mongodb.org/mongo-driver/bson/primitive"
)

//User is the model that governs all notes objects retrived or inserted into the DB
type User struct {
    ID            primitive.ObjectID `bson:"_id"`
    First_name    *string            `json:"first_name" validate:"required,min=2,max=100"`
    Last_name     *string            `json:"last_name" validate:"required,min=2,max=100"`
    Password      *string            `json:"Password" validate:"required,min=6""`
    Email         *string            `json:"email" validate:"email,required"`
    Phone         *string            `json:"phone" validate:"required"`
    Token         *string            `json:"token"`
    Refresh_token *string            `json:"refresh_token"`
    Created_at    time.Time          `json:"created_at"`
    Updated_at    time.Time          `json:"updated_at"`
    User_id       string             `json:"user_id"`
}

Enter fullscreen mode Exit fullscreen mode

Bcrypt and password management

Now let's move on to getting the bcrypt package, validator package and the gin package which will be used for password management, struct validation and route management respectively.
In your controllers/userController.go file, insert the following code

package controllers

import (
    "context"
    "fmt"
    "log"

    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"

    "user-athentication-golang/database"

    helper "user-athentication-golang/helpers"
    "user-athentication-golang/models"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "golang.org/x/crypto/bcrypt"
)

var userCollection *mongo.Collection = database.OpenCollection(database.Client, "user")
var validate = validator.New()
Enter fullscreen mode Exit fullscreen mode

Now implement password hashing and verification in your controllers/userController.go file with the following code.

//HashPassword is used to encrypt the password before it is stored in the DB
func HashPassword(password string) string {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        log.Panic(err)
    }

    return string(bytes)
}

//VerifyPassword checks the input password while verifying it with the passward in the DB.
func VerifyPassword(userPassword string, providedPassword string) (bool, string) {
    err := bcrypt.CompareHashAndPassword([]byte(providedPassword), []byte(userPassword))
    check := true
    msg := ""

    if err != nil {
        msg = fmt.Sprintf("login or passowrd is incorrect")
        check = false
    }

    return check, msg
}
Enter fullscreen mode Exit fullscreen mode

Now let's move on to implementing the signup and login service in your controllers/userController.go file with the following code. Kindly note that some functions such as helper.GenerateAllTokens and helper.UpdateAllTokens will not be available since you haven't written them yet. keep calm 😁😁

//CreateUser is the api used to tget a single user
func SignUp() gin.HandlerFunc {
    return func(c *gin.Context) {
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
        var user models.User

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

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

        count, err := userCollection.CountDocuments(ctx, bson.M{"email": user.Email})
        defer cancel()
        if err != nil {
            log.Panic(err)
            c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while checking for the email"})
            return
        }

        password := HashPassword(*user.Password)
        user.Password = &password

        count, err = userCollection.CountDocuments(ctx, bson.M{"phone": user.Phone})
        defer cancel()
        if err != nil {
            log.Panic(err)
            c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while checking for the phone number"})
            return
        }

        if count > 0 {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "this email or phone number already exists"})
            return
        }

        user.Created_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.Updated_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.ID = primitive.NewObjectID()
        user.User_id = user.ID.Hex()
        token, refreshToken, _ := helper.GenerateAllTokens(*user.Email, *user.First_name, *user.Last_name, user.User_id)
        user.Token = &token
        user.Refresh_token = &refreshToken

        resultInsertionNumber, insertErr := userCollection.InsertOne(ctx, user)
        if insertErr != nil {
            msg := fmt.Sprintf("User item was not created")
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }
        defer cancel()

        c.JSON(http.StatusOK, resultInsertionNumber)

    }
}

//Login is the api used to tget a single user
func Login() gin.HandlerFunc {
    return func(c *gin.Context) {
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
        var user models.User
        var foundUser models.User

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

        err := userCollection.FindOne(ctx, bson.M{"email": user.Email}).Decode(&foundUser)
        defer cancel()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "login or passowrd is incorrect"})
            return
        }

        passwordIsValid, msg := VerifyPassword(*user.Password, *foundUser.Password)
        defer cancel()
        if passwordIsValid != true {
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }

        token, refreshToken, _ := helper.GenerateAllTokens(*foundUser.Email, *foundUser.First_name, *foundUser.Last_name, foundUser.User_id)

        helper.UpdateAllTokens(token, refreshToken, foundUser.User_id)

        c.JSON(http.StatusOK, foundUser)

    }
}
Enter fullscreen mode Exit fullscreen mode

Token management

Now let's move on to implementing the JWT/token functionality. Get and install the JWT-GO package the app. Reference all the needed packages in the helpers/tokenHelper.go file with the import block below.

package helper

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "user-athentication-golang/database"

    jwt "github.com/dgrijalva/jwt-go"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)
Enter fullscreen mode Exit fullscreen mode

Now add a struct for all the details you'll be signing into the token. Append the following code to the helpers.tokenHelper.go file.

// SignedDetails
type SignedDetails struct {
    Email      string
    First_name string
    Last_name  string
    Uid        string
    jwt.StandardClaims
}
Enter fullscreen mode Exit fullscreen mode

Once that is done, implement the token generation function with the following code. Append it to the helpers.tokenHelper.go file.

var userCollection *mongo.Collection = database.OpenCollection(database.Client, "user")

var SECRET_KEY string = os.Getenv("SECRET_KEY")

// GenerateAllTokens generates both teh detailed token and refresh token
func GenerateAllTokens(email string, firstName string, lastName string, uid string) (signedToken string, signedRefreshToken string, err error) {
    claims := &SignedDetails{
        Email:      email,
        First_name: firstName,
        Last_name:  lastName,
        Uid:        uid,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(24)).Unix(),
        },
    }

    refreshClaims := &SignedDetails{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(168)).Unix(),
        },
    }

    token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(SECRET_KEY))
    refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(SECRET_KEY))

    if err != nil {
        log.Panic(err)
        return
    }

    return token, refreshToken, err
}
Enter fullscreen mode Exit fullscreen mode

Implement the token validation function in the helpers.tokenHelper.go file with the code below.

//ValidateToken validates the jwt token
func ValidateToken(signedToken string) (claims *SignedDetails, msg string) {
    token, err := jwt.ParseWithClaims(
        signedToken,
        &SignedDetails{},
        func(token *jwt.Token) (interface{}, error) {
            return []byte(SECRET_KEY), nil
        },
    )

    if err != nil {
        msg = err.Error()
        return
    }

    claims, ok := token.Claims.(*SignedDetails)
    if !ok {
        msg = fmt.Sprintf("the token is invalid")
        msg = err.Error()
        return
    }

    if claims.ExpiresAt < time.Now().Local().Unix() {
        msg = fmt.Sprintf("token is expired")
        msg = err.Error()
        return
    }

    return claims, msg
}
Enter fullscreen mode Exit fullscreen mode

When a user logs in, you'll need to reassign them with new tokens that expire at a later time. This simply means that you'll be updating the user's tokens in the DB whenever they login. Implement this feature in the helpers.tokenHelper.go file with the code below.

//UpdateAllTokens renews the user tokens when they login
func UpdateAllTokens(signedToken string, signedRefreshToken string, userId string) {
    var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)

    var updateObj primitive.D

    updateObj = append(updateObj, bson.E{"token", signedToken})
    updateObj = append(updateObj, bson.E{"refresh_token", signedRefreshToken})

    Updated_at, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
    updateObj = append(updateObj, bson.E{"updated_at", Updated_at})

    upsert := true
    filter := bson.M{"user_id": userId}
    opt := options.UpdateOptions{
        Upsert: &upsert,
    }

    _, err := userCollection.UpdateOne(
        ctx,
        filter,
        bson.D{
            {"$set", updateObj},
        },
        &opt,
    )
    defer cancel()

    if err != nil {
        log.Panic(err)
        return
    }

    return
}
Enter fullscreen mode Exit fullscreen mode

Middleware control

Now that you have all the functionality for the tokens ready, you can fit them into the middleware that does the checking before API calls get in the routes. To do this, insert the following code into the middleware/authMiddleware.go file

package middleware

import (
    "fmt"
    "net/http"

    helper "user-athentication-golang/helpers"

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

// Authz validates token and authorizes users
func Authentication() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientToken := c.Request.Header.Get("token")
        if clientToken == "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No Authorization header provided")})
            c.Abort()
            return
        }

        claims, err := helper.ValidateToken(clientToken)
        if err != "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err})
            c.Abort()
            return
        }

        c.Set("email", claims.Email)
        c.Set("first_name", claims.First_name)
        c.Set("last_name", claims.Last_name)
        c.Set("uid", claims.Uid)

        c.Next()

    }
}

Enter fullscreen mode Exit fullscreen mode

Setup routes

In the routes/userRoutes.go file, insert the following code to make out signup and login APIs available to the main.go file.

package routes

import (
    controller "user-athentication-golang/controllers"

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

//UserRoutes function
func UserRoutes(incomingRoutes *gin.Engine) {
    incomingRoutes.POST("/users/signup", controller.SignUp())
    incomingRoutes.POST("/users/login", controller.Login())
}

Enter fullscreen mode Exit fullscreen mode

Main.go file

Now you can round it all off with your main.go file by inserting the following code. The following code includes 2 dummy APIs which will be used to demonstrate the token's validity.
The middleware is used only after the user route because there is no need for token validation during login and signup

package main

import (
    "os"

    middleware "user-athentication-golang/middleware"
    routes "user-athentication-golang/routes"

    "github.com/gin-gonic/gin"
    _ "github.com/heroku/x/hmetrics/onload"
)

func main() {
    port := os.Getenv("PORT")

    if port == "" {
        port = "8000"
    }

    router := gin.New()
    router.Use(gin.Logger())
    routes.UserRoutes(router)

    router.Use(middleware.Authentication())

    // API-2
    router.GET("/api-1", func(c *gin.Context) {

        c.JSON(200, gin.H{"success": "Access granted for api-1"})

    })

    // API-1
    router.GET("/api-2", func(c *gin.Context) {
        c.JSON(200, gin.H{"success": "Access granted for api-2"})
    })

    router.Run(":" + port)
}
Enter fullscreen mode Exit fullscreen mode

Run and test

To run the app, first build it by running this command go build -o new -v in the terminal and then run go run main.go to allow the application listen for API requests. Your app should be available on http://localhost:8000 and your terminal should look something like this:

Alt Text

Postman

Insert the URL: http://localhost:8000 into postman and append the relevant extensions and request body if needed. Examples can be seen below.

Alt TextAlt TextAlt TextAlt Text

Conclusion

In this article, you learnt how to setup a simple golang application with a working mongoDB database. You also created a user module while implementing features such as password hashing and verification with signup and login services.
You went on to use the JWT-GO package to build a token generator and validator which was used in a middleware to validate user tokens. You can improve on this app by adding more APIs and exploring various ways of authentication.

Check the repository for this tutorial out!

Top comments (9)

Collapse
 
shreyngd profile image
shreyngd

This has been the best tutorial on golang + rest + mogo auth so far I have seen.

I have worked on nodejs express applications extensively and recently started to learn Golang. I understand the concept but its hard to code in Go.

So thankyou so much.

Other than that I also implemented this on my git. github.com/shreyngd/booker. I am also be integrating oauth2 in this example.

In the login function, we are returning foundUser at the end, which does not have updated token and refresh token in it, which can cause issues.

Collapse
 
joojodontoh profile image
JOOJO DONTOH

Hi shreyngd, thank you so much for the kind words and I'm super glad this could be of help to you. You are right about the foundUser not returning the updated token which is wrong. Thanks for spotting that. I will do my best to update the article where needed.

Collapse
 
shreyngd profile image
shreyngd

In my implementation I had used FindOneAndUpdate method github.com/shreyngd/booker/blob/26....

This method returns the updated user. I don't know whether it is correct way.

Thread Thread
 
joojodontoh profile image
JOOJO DONTOH

Yes it looks good. If you dont mind some unsolicited advice πŸ˜…, you may want to consider the renaming of the function to reflect it's concern of updating tokens only. Using the FindOneAndUpdate method is good but since the fucntion is supposed to only make an update you may resort to another function returning the user object or naming the function accordingly. This is mostly for readability and clean code purposes

Collapse
 
thanhbk113 profile image
Tran Ngoc Thanh

thanks bro, you are a good person

Collapse
 
athenalry profile image
athenalry

Hi there, what are we supposed to put in our env file for SECRET_KEY?

Collapse
 
joojodontoh profile image
JOOJO DONTOH

Hi athenalry, thanks for your question and forgive me for the late reply, I'm just seeing your message. You can choose a good, long password. Or you can generate it from a site like
grc.com/passwords.htm let me know how it goes.

Collapse
 
topoko123 profile image
topoko123

I have a question, why do we need to store tokens and refresh tokens in the database when we can write a function for decode, how can we revoke tokens?

Collapse
 
mcrrobinson profile image
Matt Robinson

I can access the API without a key in the example.