Echo framework + GORM = Blazing fast Golang server-side application. Authentication example

In this article I want to show example of login, logout and registration implementation using Go Echo framework and GORM (Go Object Relational Mapper) with PostgreSQL. We use JWT (JSON Web Token) to authenticate users.

Firstly we create main.go:

package main

import (
	"app1/helper"
	"app1/models"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	configuration := helper.GetConfig()

	gormParameters := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable", configuration.DbHost, configuration.DbPort, configuration.DbName, configuration.DbUsername, configuration.DbPassword)
	gormDB, err := gorm.Open("postgres", gormParameters)
	if err != nil {
		panic("failed to connect database")
	}
	helper.GormDB = gormDB

	// Migrate the schema (tables): User, Authentication
	helper.GormDB.AutoMigrate(&helper.User{})
	helper.GormDB.AutoMigrate(&helper.Authentication{})
	helper.GormDB.Model(&helper.Authentication{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE")

	echoFramework := echo.New()
	echoFramework.Use(middleware.Logger()) // log
	echoFramework.Use(middleware.CORS())   // CORS from Any Origin, Any Method

	echoGroupUseJWT := echoFramework.Group("/api/v1")
	echoGroupUseJWT.Use(middleware.JWT([]byte(configuration.EncryptionKey)))
	echoGroupNoJWT := echoFramework.Group("/api/v1")

	// /api/v1/users : logged in users
	echoGroupUseJWT.POST("/users/logout", models.Logout)

	// /api/v1/users : public accessible
	echoGroupNoJWT.POST("/users", models.CreateUser)
	echoGroupNoJWT.POST("/users/login", models.Login)

	defer helper.GormDB.Close()
	echoFramework.Logger.Fatal(echoFramework.Start(":1323"))
}

Secondly we create globals.go:

package helper

import (
	"regexp"
	"time"

	"github.com/jinzhu/gorm"
)

type Configuration struct {
	EncryptionKey string
	DbHost        string
	DbPort        string
	DbName        string
	DbUsername    string
	DbPassword    string
}

var configuration Configuration

type ModelBase struct {
	ID        int       `gorm:"primary_key"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

type User struct {
	ModelBase        // replaces gorm.Model
	Email     string `gorm:"not null; unique"`
	Password  string `gorm:"not null" json:"-"`
	Name      string `gorm:"not null; type:varchar(100)"` // unique_index
}

type Authentication struct {
	ModelBase
	User   User `gorm:"foreignkey:UserID; not null"`
	UserID int
	Token  string `gorm:"type:varchar(200); not null"`
}


type CustomHTTPSuccess struct {
	Data string `json:"data"`
}

type ErrorType struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type CustomHTTPError struct {
	Error ErrorType `json:"error"`
}

var GormDB *gorm.DB

func init() {
	configuration = Configuration{
		EncryptionKey: "F61L8L7CUCGN0NK6336I8TFP9Y2ZOS43",
		DbHost:        "localhost",
		DbPort:        "5432",
		DbName:        "postgres",
		DbUsername:    "postgres",
		DbPassword:    "postgres",
	}
}

func GetConfig() Configuration {
	return configuration
}

func ValidateEmail(email string) (matchedString bool) {
	re := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
	matchedString = re.MatchString(email)
	return
}

Finally our implementation of the authentication methods in user.go:

package models

import (
	"app1/helper"
	"fmt"
	"net/http"
	"strconv"

	jwt "github.com/dgrijalva/jwt-go"
	"github.com/labstack/echo"
)

func CreateUser(c echo.Context) error {
	m := echo.Map{}
	if err := c.Bind(&m); err != nil {
		// return err
	}
	name := m["name"].(string)
	email := m["email"].(string)
	password := m["password"].(string)
	confirmPassword := m["confirm_password"].(string)

	if password == "" || confirmPassword == "" || name == "" || email == "" {
		return echo.NewHTTPError(http.StatusBadRequest, "Please enter name, email and password")
	}

	if password != confirmPassword {
		return echo.NewHTTPError(http.StatusBadRequest, "Confirm password is not same to password provided")
	}

	if helper.ValidateEmail(email) == false {
		return echo.NewHTTPError(http.StatusBadRequest, "Please enter valid email")
	}

	if bCheckUserExists(email) == true {
		return echo.NewHTTPError(http.StatusBadRequest, "Email provided already exists")
	}

	configuration := helper.GetConfig()

	enc, _ := helper.EncryptString(password, configuration.EncryptionKey)

	user1 := helper.User{Name: name, Email: email, Password: enc}
	// globals.GormDB.NewRecord(user) // => returns `true` as primary key is blank
	helper.GormDB.Create(&user1)

	token := jwt.New(jwt.SigningMethodHS256)

	claims := token.Claims.(jwt.MapClaims)
	claims["name"] = user1.Name
	claims["email"] = user1.Email

	t, err := token.SignedString([]byte(configuration.EncryptionKey)) // "secret" >> EncryptionKey
	if err != nil {
		return err
	}

	authentication := helper.Authentication{}
	if helper.GormDB.First(&authentication, "user_id =?", user1.ID).RecordNotFound() {
		// insert
		helper.GormDB.Create(&helper.Authentication{User: user1, Token: t})
	} else {
		authentication.User = user1
		authentication.Token = t
		helper.GormDB.Save(&authentication)
	}

	return c.JSON(http.StatusOK, map[string]string{
		"token": t,
	})
}

func bCheckUserExists(email string) bool {
	user1 := helper.User{}
	if helper.GormDB.Where(&helper.User{Email: email}).First(&user1).RecordNotFound() {
		return false
	}
	return true
}

func ValidateUser(email, password string, c echo.Context) (bool, error) {
	fmt.Println("validate")
	var user1 helper.User
	if helper.GormDB.First(&user1, "email =?", email).RecordNotFound() {
		return false, nil
	}

	configuration := helper.GetConfig()

	decrypted, _ := helper.DecryptString(user1.Password, configuration.EncryptionKey)

	if password == decrypted {
		return true, nil
	}
	return false, nil
}

func Login(c echo.Context) error {
	m := echo.Map{}
	if err := c.Bind(&m); err != nil {
		// return err
	}
	email := m["email"].(string)
	password := m["password"].(string)

	var user1 helper.User
	if helper.GormDB.First(&user1, "email =?", email).RecordNotFound() {
		_error := helper.CustomHTTPError{
			Error: helper.ErrorType{
				Code:    http.StatusBadRequest,
				Message: "Invalid email & password",
			},
		}
		return c.JSONPretty(http.StatusBadGateway, _error, "  ")
	}

	configuration := helper.GetConfig()
	decrypted, _ := helper.DecryptString(user1.Password, configuration.EncryptionKey)

	if password == decrypted {
		token := jwt.New(jwt.SigningMethodHS256)

		claims := token.Claims.(jwt.MapClaims)
		claims["name"] = user1.Name
		claims["email"] = user1.Email
		claims["id"] = user1.ModelBase.ID

		t, err := token.SignedString([]byte(configuration.EncryptionKey)) // "secret" >> EncryptionKey
		if err != nil {
			return err
		}

		authentication := helper.Authentication{}
		if helper.GormDB.First(&authentication, "user_id =?", user1.ID).RecordNotFound() {
			// insert
			helper.GormDB.Create(&helper.Authentication{User: user1, Token: t})
		} else {
			// update
			authentication.User = user1
			authentication.Token = t
			helper.GormDB.Save(&authentication)
		}

		return c.JSON(http.StatusOK, map[string]string{
			"token": t,
		})
	} else {
		_error := helper.CustomHTTPError{
			Error: helper.ErrorType{
				Code:    http.StatusBadRequest,
				Message: "Invalid email & password",
			},
		}
		return c.JSONPretty(http.StatusBadGateway, _error, "  ")
	}
}

func Logout(c echo.Context) error {
	tokenRequester := c.Get("user").(*jwt.Token)
	claims := tokenRequester.Claims.(jwt.MapClaims)
	fRequesterID := claims["id"].(float64)
	iRequesterID := int(fRequesterID)
	sRequesterID := strconv.Itoa(iRequesterID)

	requester := helper.User{}
	if helper.GormDB.First(&requester, "id =?", sRequesterID).RecordNotFound() {
		return echo.ErrUnauthorized
	}

	authentication := helper.Authentication{}
	if helper.GormDB.First(&authentication, "user_id =?", requester.ModelBase.ID).RecordNotFound() {
		return echo.ErrUnauthorized
	}
	helper.GormDB.Delete(&authentication)
	return c.String(http.StatusAccepted, "")
}

 

Encryption and decryption functions are missing. Drop me an email to get them 🙂

Leave a Reply