Creating JWT's and SignUp Route - Part[2/3] of Go Authentication series
Tags: go, security, beginners
Published: 2020-10-31 06:51:50

In this part, we will configure the SignUp route for our server.

# Getting Started
For the authentication process of the user, we will be using JSON Web Tokens(JWT's). I am gonna assume you know what a JWT is. If you want to learn about JWT, [here is a very good introduction to it](https://jwt.io/introduction/).

We will first start by creating some new models inside our **models/user.go** file.

We will need to add a model called _Claims_ and a model called _UserErrors_ inside the file. The _Claims_ will contain the struct of the information that the JWT token will contain. And the _UserErrors_ will contain the return body if there is an error during the registration process.

**models/user.go**
```go
package models

++ import (
++ 	"github.com/dgrijalva/jwt-go"
++ )

// User represents a User schema
type User struct {
	Base
	Email    string `json:"email" gorm:"unique"`
	Username string `json:"username" gorm:"unique"`
	Password string `json:"password"`
}

++ // UserErrors represent the error format for user routes
++ type UserErrors struct {
++	Err      bool   `json:"error"`
++	Email    string `json:"email"`
++	Username string `json:"username"`
++	Password string `json:"password"`
++ }

++ // Claims represent the structure of the JWT token
++ type Claims struct {
++	jwt.StandardClaims
++	ID uint `gorm:"primaryKey"`
++ }
```

## Auto Migrate the data
Now that we will store data in our database we need to migrate our data. We will use the AutoMigrate function from Gorm for this.

**database/postgres.go**
```go
package database

import (
	"fmt"
	"go-authentication-boilerplate/models"
	"log"
	"os"

	"github.com/joho/godotenv"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// DB represents a Database instance
var DB *gorm.DB

// ConnectToDB connects the server with database
func ConnectToDB() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading env file \n", err)
	}

	dsn := fmt.Sprintf("host=localhost user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Kolkata",
		os.Getenv("PSQL_USER"), os.Getenv("PSQL_PASS"), os.Getenv("PSQL_DBNAME"), os.Getenv("PSQL_PORT"))

	log.Print("Connecting to Postgres DB...")
	DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to database. \n", err)
		os.Exit(2)

	}
	log.Println("connected")

	// turned on the loger on info mode
	DB.Logger = logger.Default.LogMode(logger.Info)

++	log.Print("Running the migrations...")
++	DB.AutoMigrate(&models.User{}, &models.Claims{})
}
```

### Creating Utility Functions
We will be creating two tokens, an **access_token** and a **refresh_token**. We will create tokens using a Go library called [jwt-go](https://github.com/dgrijalva/jwt-go).

Then we will create a middleware that validates the _access_token_ stored in the Request's cookie then another function that returns token cookies when called.

Now, we need to create a folder named _util_. Inside _util_ we will create a new file called _auth.go_. Right now, this file will contain 5 different functions.

1. **GenerateTokens** - This function will return access_token and refresh_token.
2. **GenerateAccessClaims** - GenerateAccessClaims generates and returns a claim and a acess_token string. This will create an access token with an expiry of 15 minutes.
3. **GenerateRefreshClaims** - GenerateRefreshClaims generates and returns a refresh_token string. This will create a refresh token with an expiry of 30 days.
4. **SecureAuth** - SecureAuth returns a middleware which secures all the private routes. This middleware first validates the access_toke. If the token is valid then we will use _Locals_ which is a method that stores variables scoped to the request. 
5. **GetAuthCookies** - GetAuthCookies sends two cookies of type access_token and refresh_token.

**util/auth.go**
```go
package util

import (
	db "go-authentication-boilerplate/database"
	"go-authentication-boilerplate/models"
	"time"
    "os"

	"github.com/dgrijalva/jwt-go"
	"github.com/gofiber/fiber/v2"
)

var jwtKey = []byte(os.Getenv("PRIV_KEY"))

// GenerateTokens returns the access and refresh tokens
func GenerateTokens(uuid string) (string, string) {
	claim, accessToken := GenerateAccessClaims(uuid)
	refreshToken := GenerateRefreshClaims(claim)

	return accessToken, refreshToken
}

// GenerateAccessClaims returns a claim and a acess_token string
func GenerateAccessClaims(uuid string) (*models.Claims, string) {

	t := time.Now()
	claim := &models.Claims{
		StandardClaims: jwt.StandardClaims{
			Issuer:    uuid,
			ExpiresAt: t.Add(15 * time.Minute).Unix(),
			Subject:   "access_token",
			IssuedAt:  t.Unix(),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		panic(err)
	}

	return claim, tokenString
}

// GenerateRefreshClaims returns refresh_token
func GenerateRefreshClaims(cl *models.Claims) string {
	result := db.DB.Where(&models.Claims{
		StandardClaims: jwt.StandardClaims{
			Issuer: cl.Issuer,
		},
	}).Find(&models.Claims{})

	// checking the number of refresh tokens stored.
	// If the number is higher than 3, remove all the refresh tokens and leave only new one.
	if result.RowsAffected > 3 {
		db.DB.Where(&models.Claims{
			StandardClaims: jwt.StandardClaims{Issuer: cl.Issuer},
		}).Delete(&models.Claims{})
	}

	t := time.Now()
	refreshClaim := &models.Claims{
		StandardClaims: jwt.StandardClaims{
			Issuer:    cl.Issuer,
			ExpiresAt: t.Add(30 * 24 * time.Hour).Unix(),
			Subject:   "refresh_token",
			IssuedAt:  t.Unix(),
		},
	}

	// create a claim on DB
	db.DB.Create(&refreshClaim)

	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaim)
	refreshTokenString, err := refreshToken.SignedString(jwtKey)
	if err != nil {
		panic(err)
	}

	return refreshTokenString
}

// SecureAuth returns a middleware which secures all the private routes
func SecureAuth() func(*fiber.Ctx) error {
	return func(c *fiber.Ctx) error {
		accessToken := c.Cookies("access_token")
		claims := new(models.Claims)

		token, err := jwt.ParseWithClaims(accessToken, claims,
			func(token *jwt.Token) (interface{}, error) {
				return jwtKey, nil
			})

		if token.Valid {
			if claims.ExpiresAt < time.Now().Unix() {
				return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
					"error":   true,
					"general": "Token Expired",
				})
			}
		} else if ve, ok := err.(*jwt.ValidationError); ok {
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				// this is not even a token, we should delete the cookies here
				c.ClearCookie("access_token", "refresh_token")
				return c.SendStatus(fiber.StatusForbidden)
			} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
				// Token is either expired or not active yet
				return c.SendStatus(fiber.StatusUnauthorized)
			} else {
				// cannot handle this token
				c.ClearCookie("access_token", "refresh_token")
				return c.SendStatus(fiber.StatusForbidden)
			}
		}

		c.Locals("id", claims.Issuer)
		return c.Next()
	}
}

// GetAuthCookies sends two cookies of type access_token and refresh_token
func GetAuthCookies(accessToken, refreshToken string) (*fiber.Cookie, *fiber.Cookie) {
	accessCookie := &fiber.Cookie{
		Name:     "access_token",
		Value:    accessToken,
		Expires:  time.Now().Add(24 * time.Hour),
		HTTPOnly: true,
		Secure:   true,
	}

	refreshCookie := &fiber.Cookie{
		Name:     "refresh_token",
		Value:    refreshToken,
		Expires:  time.Now().Add(10 * 24 * time.Hour),
		HTTPOnly: true,
		Secure:   true,
	}

	return accessCookie, refreshCookie
}
```

Now, we will need to create some validator functions to validate the input send while registering the user.
 
So we do this by creating a new file called _validators.go_ inside the _util_ folder.

**util/validators.go**
```go
package util

import (
	"go-authentication-boilerplate/models"
	"regexp"

	valid "github.com/asaskevich/govalidator"
)

// IsEmpty checks if a string is empty
func IsEmpty(str string) (bool, string) {
	if valid.HasWhitespaceOnly(str) && str != "" {
		return true, "Must not be empty"
	}

	return false, ""
}

// ValidateRegister func validates the body of user for registration
func ValidateRegister(u *models.User) *models.UserErrors {
	e := &models.UserErrors{}
	e.Err, e.Username = IsEmpty(u.Username)

	if !valid.IsEmail(u.Email) {
		e.Err, e.Email = true, "Must be a valid email"
	}

	re := regexp.MustCompile("\\d") // regex check for at least one integer in string
	if !(len(u.Password) >= 8 && valid.HasLowerCase(u.Password) && valid.HasUpperCase(u.Password) && re.MatchString(u.Password)) {
		e.Err, e.Password = true, "Length of password should be atleast 8 and it must be a combination of uppercase letters, lowercase letters and numbers"
	}

	return e
}
```

### Creating a SignUp Route
First, we will modify the SetupRoutes function inside _router/setup.go_ file.

**router/setup.go**
```go
package router

import (
	"github.com/gofiber/fiber/v2"
)

++ // USER handles all the user routes
++ var USER fiber.Router

// SetupRoutes setups all the Routes
func SetupRoutes(app *fiber.App) {
	api := app.Group("/api")

++	USER = api.Group("/user")
++	SetupUserRoutes()
}
```

Now we will create a new file called _users.go_ inside the _router_ folder.

Next, we will create our SignUp route(finally!) inside that file. 
We will use the following steps to register the user:

1. Parse the input data into a User model struct.
2. Validate the input by calling the _ValidateRegister_ function from _util/validators.go_.
3. Check that the email and username are unique.
4. If all is well till now, then hash the password using [bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) library with a random salt.
5. Now, register the user inside our Database and generate the access and refresh tokens.
6. Set the access and refresh token as cookies with httpOnly and secure flag.
7. Return the tokens.

Following all these steps, our _router/user.go_ file will look like this:

**router/user.go**
```go
package router

import (
	db "go-authentication-boilerplate/database"
	"go-authentication-boilerplate/models"
	"go-authentication-boilerplate/util"
	"math/rand"
	"time"

	"golang.org/x/crypto/bcrypt"

	"github.com/dgrijalva/jwt-go"
	"github.com/gofiber/fiber/v2"
)

var jwtKey = []byte(os.Getenv("PRIV_KEY"))

// SetupUserRoutes func sets up all the user routes
func SetupUserRoutes() {
	USER.Post("/signup", CreateUser)              // Sign Up a user

}

// CreateUser route registers a User into the database
func CreateUser(c *fiber.Ctx) error {
	u := new(models.User)

	if err := c.BodyParser(u); err != nil {
		return c.JSON(fiber.Map{
			"error": true,
			"input": "Please review your input",
		})
	}

	// validate if the email, username and password are in correct format
	errors := util.ValidateRegister(u)
	if errors.Err {
		return c.JSON(errors)
	}

	if count := db.DB.Where(&models.User{Email: u.Email}).First(new(models.User)).RowsAffected; count > 0 {
		errors.Err, errors.Email = true, "Email is already registered"
	}
	if count := db.DB.Where(&models.User{Username: u.Username}).First(new(models.User)).RowsAffected; count > 0 {
		errors.Err, errors.Username = true, "Username is already registered"
	}
	if errors.Err {
		return c.JSON(errors)
	}

	// Hashing the password with a random salt
	password := []byte(u.Password)
	hashedPassword, err := bcrypt.GenerateFromPassword(
		password,
		rand.Intn(bcrypt.MaxCost-bcrypt.MinCost)+bcrypt.MinCost,
	)

	if err != nil {
		panic(err)
	}
	u.Password = string(hashedPassword)

	if err := db.DB.Create(&u).Error; err != nil {
		return c.JSON(fiber.Map{
			"error":   true,
			"general": "Something went wrong, please try again later. 😕",
		})
	}

	// setting up the authorization cookies
	accessToken, refreshToken := util.GenerateTokens(u.UUID.String())
	accessCookie, refreshCookie := util.GetAuthCookies(accessToken, refreshToken)
	c.Cookie(accessCookie)
	c.Cookie(refreshCookie)

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"access_token":  accessToken,
		"refresh_token": refreshToken,
	})
}
```

Now that we have created our SignUp Route, we can start working on our SignIn route on the next Part.

_Thanks for reading! If you liked this article, please let me know and share it!_