DEV Community

Cover image for Build user type based authorization in Golang and mongoDB
JOOJO DONTOH
JOOJO DONTOH

Posted on

Build user type based authorization in Golang and mongoDB

This post is an extension of this article therefore if you happen to be a beginner, kindly familiarize yourself with the JWT concept before going through this post.

After a user gains access to a server/system there may be the need to limit access to different resources based on the user's type or role. This article will walk through the process of building a simple authorization system based on the user's type. In the last article concerning JWT (Build user authentication in Golang with JWT and mongoDB), user details such as the user's email, first name, last name and user ID were signed into the token to be made available for various possible use cases throughout the system. An important use case as stated earlier may be to authorize or grant users the permission to access some system resources.

Development Overview

  1. Model.
    • Add User_type to user model
  2. Token.
    • Add user type to token generation process
  3. Login and sign up
    • Add User_type to both the signup and login service
  4. Authentication
    • Bind User_type to the gin object
  5. Build user type authorization functions.
  6. Create user-list and single-user endpoints and add the authorization functions to these endpoints.

Model

Building on from this article, the User_type attribute must be added to the user model to determine the role of the user. Kindly add the code below to the models.userModel.go file.

package models

import (
    "time"

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

//User is the model that governs all notes objects retrieved 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"`

//new code
    User_type     *string            `json:"user_type" 
validate:"required,eq=ADMIN|eq=USER""`


    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

Token

Since the usertype attribute has now been added to the model, we can add it to the items to be signed into the token during the token generation process. kindly add the following //new code to the GenerateAllTokens function in the helpers.tokenHelper.go file.

func GenerateAllTokens(email string, firstName string, lastName string, 

//new code
userType string,
//new code

uid string) (signedToken string, signedRefreshToken string, err error) {
    claims := &SignedDetails{
        Email:      email,
        First_name: firstName,
        Last_name:  lastName,
        Uid:        uid,

//new code
        User_type:  userType,
//new code


        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

Login and sign up

The GenerateAllTokens function is used in the both the sign up and login functions to create and renew tokens for users respectively. Therefore it is only right the user type is added as a parameter to the referenced function to update it. Note that in a production grade application it is not advisable to add the usertype to the signup. This is simply to demonstrate the core concepts of user access to endpoints. Add the //new code below to the Login function in the controllers.userController.go file.

//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, 

//new code
*foundUser.User_type, 
//newcode

foundUser.User_id)

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

        c.JSON(http.StatusOK, foundUser)

    }
}
Enter fullscreen mode Exit fullscreen mode

Also add the //new code below to the SignUp function in the controllers.userController.go file.

//CreateUser is the api used to get 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, 

//new code
*user.User_type, 
//new code

*&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)

    }
}
Enter fullscreen mode Exit fullscreen mode

Authentication

Now during the authentication process, the user type must be bound to the gin context object. This should be done by setting the attribute to a string key within the gin context object. Add the following //new code below to the Authentication() function in the middleware.authMiddleware.go file.

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)

//new code
        c.Set("user_type", claims.User_type)
//new code

        c.Next()

    }
}
Enter fullscreen mode Exit fullscreen mode

User type checker

Now that the user type attribute can successfully be added and retrieved to and from the token, a CheckUserType and MatchUserTypeToUid function must be built to monitor access to various endpoints. Create a authHelper.go in the helpers file to contain the CheckUserType and MatchUserTypeToUid function. Add the code below to the authHelper.go file.

package helper

import (
    "errors"

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

//CheckUserType renews the user tokens when they login
func CheckUserType(c *gin.Context, role string) (err error) {
    userType := c.GetString("user_type")
    err = nil
    if userType != role {
        err = errors.New("Unauthorized to access this resource")
        return err
    }

    return err
}


//MatchUserTypeToUid only allows the user to access their data and no other data. Only the admin can access all user data
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("Unauthorized to access this resource")
        return err
    }
    err = CheckUserType(c, userType)

    return err
}

Enter fullscreen mode Exit fullscreen mode

User list and single user endpoints

These API endpoints will be written and used to demonstrate how different user types may have access to different system resources. The CheckUserType and MatchUserTypeToUid function references have already been added. Add the code below to the controllers.userController.go file.

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 := 10
        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{{"$match", bson.D{{}}}}
        groupStage := bson.D{{"$group", bson.D{{"_id", bson.D{{"_id", "null"}}}, {"total_count", bson.D{{"$sum", 1}}}, {"data", bson.D{{"$push", "$$ROOT"}}}}}}
        projectStage := bson.D{
            {"$project", bson.D{
                {"_id", 0},
                {"total_count", 1},
                {"user_items", bson.D{{"$slice", []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])

    }
}

//GetUser is the api used to tget a single user
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)

    }
}
Enter fullscreen mode Exit fullscreen mode

Testing

Create a few users (at least 2) with either USER or ADMIN user types for testing purposes. This may be done through the signup API. Note that in a production grade application it is not advisable to add the usertype to the signup.

Alt Text

Run both the /users and /users/:user_id endpoints with tokens from both ADMIN and USER users to verify the access levels. If all the code was written well, only ADMIN users should have access to the /users API endpoint. ADMIN users should also have access to all user data in the /users/:user_id endpoint while a USER user should have access to just their personal data. An error should be thrown if these rules aren't met.

Alt Text

Alt Text

Conclusion

This post builds on this article: Build user authentication in Golang with JWT and mongoDB to describe the process of developing user type based authorization. This post first adds the user type attribute to the previously built model and then also adds the attribute to the token generation process. This post then updates the login and signup process by also adding the user type attribute the respective endpoints. The attribute is then bound to the authentication process and checked during the use of the newly created /users and /users/:user_id endpoints.

Check the repository for this tutorial out!

Top comments (0)