Hello fellow developers❤!
In this blog, we're jumping headfirst into the creation of a JWT authentication application using the powerful Gin Web Framework.
Grab your coffee, as we go on an goLang adventure filled with code, creativity, and countless "aha!" moments😆.
But wait, there's more! Along the way, we'll learn of JWT - Authentication, learn when to wield its power and master the art of building APIs for a signup, login, user retrieval, and more.
And fear not, brave coder! I'll be your guide through every twist and turn, so you can learn with ease🌟
What should We Have Beforehand?
- Set up the directory structure for your project.
- Understand Mod Files, Packages, and Functions.
- Familiarize yourself with concepts like Struct, Slices, Routing, and Database Connection.
- For Database Connection, use MongoDB Atlas. Understand Clusters and how to fetch your Connection String from MongoDB Atlas and integrate it into your code.
JWT Authentication
- JWT consists of three parts: header, payload, and signature, separated by dots.
- The header contains token type (JWT) and signing algorithm (e.g., RSA, HMAC).
- Payload holds claims (user information) and additional data (e.g., expiration time).
- Signature, created with a secret key and secure algorithm (e.g., HMAC, SHA256), ensures authentication and data integrity.
Folder Structure for Golang Gin Framework
Build JWT Authentication using Golang and MongoDB
Fill Your main.go
As is already known, "main.go" will be the first file we create. I'll put a few packages, here.
In the `func main`, I set the `port` variable to 8000 and created a new router with `gin.New()`. Optionally, I added middleware for logging with `router.Use(gin.Logger())`.
Two functions, `UserRoutes` and `AuthRoutes`, are then called from the `routes` package, passing the router variable to them. While this is a concise approach, it's good practice to consider using environment variables for configuration, like fetching the port number from a `.env` file. This approach requires importing the `os` and `github.com/joho/godotenv` packages.
package main
import (
"github.com/gin-gonic/gin"
routes "github.com/golangcompany/JWT-Authentication/routes"
)
func main() {
port := "3000"
router := gin.New()
routes.UserRoutes(router)
routes.AuthRoutes(router)
router.GET("/get-api", func(c *gin.Context) {
c.JSON(200, gin.H{"success": "Successfully fetched 1"})
})
router.GET("/get-api-2", func(c *gin.Context) {
c.JSON(200, gin.H{"success": "Successfully fetched 12"})
})
router.Run(":" + port)
}
Create a Struct
In userModels.go, I defined a struct with fields including ID, First Name, Last Name, Password, E-Mail, Phone Number, Token ID, User Type, Refresh Token ID, Created at, Updated at, and User ID.
I imported the "primitive" package for generating unique IDs. The validate function ensures client-entered data matches our format. For timestamps like Created at and Updated at, I used the time.Time data type and imported the time package.
package models
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"time"
)
type Users struct {
ID primitive.ObjectID `bson:"_id"`
FirstName *string `json:"firstName" validate:"required,min=2,max=100"`
LastName *string `json:"lastName" 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"`
UserType *string `json:"userType" validate:"required,eq=ADMIN|eq=USER"`
RefreshToken *string `json:"refresh_token"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
UserId string `json:"userId"`
}
Now Comes Your Route
In the `routes.go` file, we define the `UserRoutes` function, which handles two POST methods for signup and login.
The paths for these methods are "/users/signup" and "/users/login".
package routes
import (
"github.com/gin-gonic/gin"
controller "github.com/golangcompany/JWT-Authentication/controllers"
"github.com/golangcompany/JWT-Authentication/middleware"
)
func UserRoutes(routes *gin.Engine) {
routes.POST("users-signup", controller.AuthSignup())
routes.POST("users-login", controller.AuthLogin())
}
func AuthRoutes(incomingRoutes *gin.Engine) {
routes.Use(middleware.UserAuthenticate())
routes.GET("/usersData", controller.GetUsers())
routes.GET("/users/:user_id", controller.GetUser())
}
Each method calls a corresponding function in the controller package. Additionally, we define the `AuthRoutes` function, which uses the GET method and the `middleware.Authenticate` function to protect routes. This ensures that only authenticated users can access these routes.
package database
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func DBSet() *mongo.Client {
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb+srv://codegirl_forya:@xxxxx.xxxxx.mongodb.net/?retryWrites=true&w=majority"))
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Connect MongoDB with Golang Gin
I'll create a function called `DBSet` that connects to MongoDB and returns the client object if successful, otherwise `nil`. The URI is stored in the `client` variable.
To connect to MongoDB, know the connection string. Then, I'll declare a variable called `Client` of type `*mongo.Client`, initialized with the return value of `DBSet()`.
Finally, I'll define a function called `UserData` to access a collection. It takes a client pointer and collection name as parameters. After establishing the connection, I'll proceed with `userControllers.go`.
package database
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func SetDB() *mongo.Client {
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb+srv://codegirl_forya:@xxxxx.xxxxx.mongodb.net/?retryWrites=true&w=majority"))
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)
}
err = client.Ping(context.TODO(), nil)
if err != nil {
log.Println("Failed to Connect")
return nil
}
fmt.Println("Successfully Connected to the MongoDB")
return client
}
var Client *mongo.Client = SetDB()
func UserData(client *mongo.Client, CollectionName string) *mongo.Collection {
var collection *mongo.Collection = client.Database("Cluster0").Collection(CollectionName)
return collection
}
Control with Controllers
package controllers
import (
"context"
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/golangcompany/JWT-Authentication/database"
helper "github.com/golangcompany/JWT-Authentication/helpers"
"github.com/golangcompany/JWT-Authentication/models"
"golang.org/x/crypto/bcrypt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
This is how i set up the mongoDB connection
var usersCollection *mongo.Collection = database.UsersData(database.Client, "user")
var validate = validator.New()
The HashingPassword will make the password in hash form for security purpose.
func HashingPassword(password string) string {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
if err != nil {
log.Panic(err)
}
return string(bytes)
}
The VerifyPassword will verify the password of an user.
func VerifyPassword(userPassword string, providedPassword string) (bool, string) {
err := bcrypt.CompareHashAndPassword([]byte(providedPassword), []byte(userPassword))
check := true
msg := ""
if err != nil {
msg = fmt.Sprintf("E-Mail or Password is incorrect")
check = false
}
return check, msg
}
The Signup func will create a new user with every necessary details.
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 detected while fetching the email"})
}
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 fetching the phone number"})
}
if count > 0 {
c.JSON(http.StatusInternalServerError, gin.H{"Error": "The mentioned E-Mail or Phone Number already exists"})
}
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_type, *&user.User_id)
user.Token = &token
user.Refresh_token = &refreshToken
resultInsertionNumber, insertErr := userCollection.InsertOne(ctx, user)
if insertErr != nil {
msg := fmt.Sprintf("User Details were not Saved")
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}
defer cancel()
c.JSON(http.StatusOK, resultInsertionNumber)
}
}
The login function will verify the credentials, the user is providing. Based on that a jwt token and refresh token will generate.
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": "email or password is incorrect"})
return
}
passwordIsValid, msg := VerifyPassword(*user.Password, *foundUser.Password)
defer cancel()
if passwordIsValid != true {
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}
if foundUser.Email == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "user not found"})
}
token, refreshToken, _ := helper.GenerateAllTokens(*foundUser.Email, *foundUser.First_name, *foundUser.Last_name, *foundUser.User_type, foundUser.User_id)
helper.UpdateAllTokens(token, refreshToken, foundUser.User_id)
err = userCollection.FindOne(ctx, bson.M{"user_id": foundUser.User_id}).Decode(&foundUser)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, foundUser)
}
}
The GetUsers func will gather all the users who is of type "USER".
func GetUsers() gin.HandlerFunc {
return func(c *gin.Context) {
if err := helper.CheckUserType(c, "ADMIN"); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
recordPerPage, err := strconv.Atoi(c.Query("recordPerPage"))
if err != nil || recordPerPage < 1 {
recordPerPage = 10
}
page, err1 := strconv.Atoi(c.Query("page"))
if err1 != nil || page < 1 {
page = 1
}
startIndex := (page - 1) * recordPerPage
startIndex, err = strconv.Atoi(c.Query("startIndex"))
matchStage := bson.D{{Key: "$match", Value: bson.D{{}}}}
groupStage := bson.D{{Key: "$group", Value: bson.D{
{Key: "_id", Value: bson.D{{Key: "_id", Value: "null"}}},
{Key: "total_count", Value: bson.D{{Key: "$sum", Value: 1}}},
{Key: "data", Value: bson.D{{Key: "$push", Value: "$$ROOT"}}}}}}
projectStage := bson.D{
{Key: "$project", Value: bson.D{
{Key: "_id", Value: 0},
{Key: "total_count", Value: 1},
{Key: "user_items", Value: bson.D{{Key: "$slice", Value: []interface{}{"$data", startIndex, recordPerPage}}}}}}}
result, err := userCollection.Aggregate(ctx, mongo.Pipeline{
matchStage, groupStage, projectStage})
defer cancel()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while listing user items"})
}
var allusers []bson.M
if err = result.All(ctx, &allusers); err != nil {
log.Fatal(err)
}
c.JSON(http.StatusOK, allusers[0])
}
}
You can fetch users by specific user_id.
func GetUser() gin.HandlerFunc {
return func(c *gin.Context) {
userId := c.Param("user_id")
if err := helper.MatchUserTypeToUid(c, userId); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
var user models.User
err := userCollection.FindOne(ctx, bson.M{"user_id": userId}).Decode(&user)
defer cancel()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
}
Is Middleware Necessary?
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
helper "github.com/golangcompany/JWT-Authentication/helpers"
"net/http"
)
func Authenticate() 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.Set("user_type", claims.User_type)
c.Next()
}
}
Helpers will Help You
package helper
import (
"errors"
"github.com/gin-gonic/gin"
)
func CheckUserType(c *gin.Context, role string) (err error) {
userType := c.GetString("user_type")
err = nil
if userType != role {
err = errors.New("You are not authorised to access this resource")
return err
}
return err
}
func MatchUserTypeToUid(c *gin.Context, userId string) (err error) {
userType := c.GetString("user_type")
uid := c.GetString("uid")
err = nil
if userType == "USER" && uid != userId {
err = errors.New("You are not authorised to access this resource")
return err
}
err = CheckUserType(c, userType)
return err
}
authHelper.go manages user authentication with functions for checking user roles and matching user types to IDs. tokenHelper.go handles JWT token generation, validation, and database updates, including functions for generating, validating, and updating tokens.
package helper
import (
"context"
"fmt"
jwt "github.com/dgrijalva/jwt-go"
"github.com/golangcompany/JWT-Authentication/database"
"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"
"log"
"os"
"time"
)
type SignedDetails struct {
Email string
First_name string
Last_name string
Uid string
User_type string
jwt.StandardClaims
}
var userCollection *mongo.Collection = database.UserData(database.Client, "user")
var SECRET_KEY string = os.Getenv("SECRET_KEY")
func GenerateAllTokens(email string, firstName string, lastName string, userType string, uid string) (signedToken string, signedRefreshToken string, err error) {
claims := &SignedDetails{
Email: email,
First_name: firstName,
Last_name: lastName,
Uid: uid,
User_type: userType,
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
}
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
}
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{Key: "token", Value: signedToken})
updateObj = append(updateObj, bson.E{Key: "refresh_token", Value: signedRefreshToken})
Updated_at, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
updateObj = append(updateObj, bson.E{Key: "updated_at", Value: Updated_at})
upsert := true
filter := bson.M{"user_id": userId}
opt := options.UpdateOptions{
Upsert: &upsert,
}
_, err := userCollection.UpdateOne(
ctx,
filter,
bson.D{
{Key: "$set", Value: updateObj},
},
&opt,
)
defer cancel()
if err != nil {
log.Panic(err)
return
}
return
}
Now check your APIs on Postman and get your desired output👀
Happy coding✌