DEV Community

Cover image for Creating JWT's and SignUp Route - Part[2/3] of Go Authentication series
Faizan
Faizan

Posted on

Creating JWT's and SignUp Route - Part[2/3] of Go Authentication series

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.

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

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"`
++ }
Enter fullscreen mode Exit fullscreen mode

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

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{})
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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
}
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

Creating a SignUp Route

First, we will modify the SetupRoutes function inside router/setup.go file.

router/setup.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()
}
Enter fullscreen mode Exit fullscreen mode

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

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,
    })
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
jonahbutler profile image
jonah-butler

Thanks for the great and informative article!
One question though, why would the cookie expiration on the access token be 24 hours when the expiration on the actual jwt access token is shorter? Is there a reason to have those expirations be different?

Collapse
 
orlovssky profile image
Sanzhar A

Hello! Why did you add this snippet? i cannot find the usage

// AfterUpdate will update the Base struct after every update
func (base *Base) AfterUpdate(tx *gorm.DB) error {
// update timestamps
base.UpdatedAt = GenerateISOString()
return nil
}

Collapse
 
mdfaizan7 profile image
Faizan

This is to update the updatedAt field after every update on the database