JOOJO DONTOH
Posted on February 3, 2021
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
- First the user logs in from a client device which makes an API call to the server with the user's details
- The server verifies these details from the user to make sure the user exists in the system
- Once verification is complete, the server sends a secure token to client device.
- The received token can now be used by the client to grant access to api resources for the user.
Development Overview
- Setup.
- Application setup
- Database setup
- Create a User module.
- User model
- Install and implement the bcrypt package.
- Password hashing
- Password verification upon login
- Sign up service
- Login Service
- Install and implement the JWT-GO package.
- build token generator
- build token validator
- update token upon login
- Implement JWT middleware for validation.
- Setup routes.
- Write main.go file.
Let's begin!
This is the file structure you'll be using
Setting up the application
$ go build
$ ./new
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}}
- 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
}
- 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
- 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"`
}
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()
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
}
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)
}
}
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"
)
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
}
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
}
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
}
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
}
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()
}
}
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())
}
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)
}
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:
Postman
Insert the URL: http://localhost:8000
into postman and append the relevant extensions and request body if needed. Examples can be seen below.
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!
Posted on February 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.