DEV Community

loading...
Cover image for Build user authentication in Golang with JWT and mongoDB

Build user authentication in Golang with JWT and mongoDB

joojodontoh profile image JOOJO DONTOH Updated on ・10 min read

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!

Discussion (2)

pic
Editor guide
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 Author

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.