Build user type based authorization in Golang and mongoDB
Published: 2021-02-07 14:36:42

>*This post is an extension of [this article](https://dev.to/joojodontoh/build-user-authentication-in-golang-with-jwt-and-mongodb-2igd) 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](https://dev.to/joojodontoh/build-user-authentication-in-golang-with-jwt-and-mongodb-2igd)), 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](https://pkg.go.dev/github.com/gin-gonic/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](https://dev.to/joojodontoh/build-user-authentication-in-golang-with-jwt-and-mongodb-2igd), 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"`
}

```

####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
}
```

####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)

	}
}
```
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)

	}
}
```

####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()

	}
}
```

####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
}

```

####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)

	}
}
``` 

####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](https://dev-to-uploads.s3.amazonaws.com/i/juqng6ljhjnsla6fceej.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/a93iw8dj1zu2os27ndrg.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2vea87e5dmg1aeesgp6l.png)

###Conclusion

This post builds on this article: [Build user authentication in Golang with JWT and mongoDB](https://dev.to/joojodontoh/build-user-authentication-in-golang-with-jwt-and-mongodb-2igd) 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] (https://github.com/Joojo7/user-athentication-golang) for this tutorial out!