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