In this article, we will build an authentication API with two endpoints, a register
endpoint to sign up users and a login
endpoint. Then we will add two endpoints that will need authentication and authorization to get access. For this article will be an endpoint to get a list of groceries and an endpoint to add a grocery to the database.
We will use the Gin framework and GORM as ORM.
Prerequisites:
- Go basic knowledge
- JWT knowledge
- Gin
- CRUD operations with GORM
This will be our directory's structure:
handlers/
auth.go
grocery.go
models/
database.go
user.go
grocery.go
middleware/
jwtMiddleware.go
utils/
token.go
main.go
.env
Briefly, I will explain these packages. In the models
module, we have the setup for our database, the model for users with functions to verify and hash the password, and the model for groceries. Every user will be able to add groceries and retrieve all the groceries added.
In the handlers
module, we have the handlers for registering and login users. Handlers for retrieving and adding groceries.
In the middleware
module, we have the jwtMiddleware
to handle incoming requests and ensure the users are authenticated.
We place all the functions related to generating JWT tokens, getting tokens, and parsing JWT in utils
.
Setup
First, let's create our module:
go mod init <your module name>
Then we install the packages we are going to use:
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u github.com/golang-jwt/jwt/v4
go get -u github.com/joho/godotenv
go get -u golang.org/x/crypto
In our root directory, we create a package for our model and file to set up our database:
user.go
package models
import (
"gorm.io/gorm"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"-"`
}
In this file, we create a User struct for our model with two fields: Username and Password. But also gorm.Model
adds the fields: ID, CreatedAt, UpdatedAt, and DeletedAt.
databaseSetup.go
package models
import (
"fmt"
"github.com/joho/godotenv"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"log"
"os"
)
func Setup() (*gorm.DB, error) {
err := godotenv.Load()
if err != nil {
log.Println("Error loading .env file")
}
dbUrl := fmt.Sprint(os.Getenv("DATABASE_URL"))
db, err := gorm.Open(sqlite.Open(dbUrl), &gorm.Config{})
if err != nil {
log.Fatal(err.Error())
}
if err = db.AutoMigrate(&User{}); err != nil {
log.Println(err)
}
return db, err
}
In this file we create a database using SQLite, we pass the database URL to sqlite.Open()
.
Register Handler
models/user.go
package models
import (
"errors"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"html"
"log"
"strings"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"-"`
}
func (user *User) HashPassword() error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
user.Password = string(hashedPassword)
user.Username = html.EscapeString(strings.TrimSpace(user.Username))
return nil
}
In the user.go
file we add the function to hash the password using bcrypt
library.
auth.go
package handlers
import (
"github.com/carlosm27/jwtGinApi/models"
"github.com/carlosm27/jwtGinApi/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type Server struct {
db *gorm.DB
}
func NewServer(db *gorm.DB) *Server {
return &Server{db: db}
}
func (s *Server) Register(c *gin.Context) {
var input RegisterInput
if err := c.ShouldBind(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := models.User{Username: input.Username, Password: input.Password}
user.HashPassword()
if err := s.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
c.JSON(http.StatusCreated , gin.H{"message": "User created"})
}
func (s *Server) Login(c *gin.Context) {
}
Here, we develop our register and login handlers. The login handler will be developed further in this article.
The register
handler takes the username and password passed by the user and binds them to the variable input
, if any or some field is missed, it will send a BadRequest
status code.
Then, the username and password are passed to the user
variable to create a User instance. Then we pass the variable user
as a reference to the create()
function after we hash the password, to create a user in our database.
If there is no problem creating a user, the server will send the message "User created".
main.go
package main
import (
"github.com/carlosm27/jwtGinApi/models"
"github.com/carlosm27/jwtGinApi/handlers"
"github.com/carlosm27/jwtGinApi/middleware"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"gorm.io/gorm"
"log"
"os"
)
func DbInit() *gorm.DB {
db, err := models.Setup()
if err != nil {
log.Println("Problem setting up database")
}
return db
}
func SetupRouter() *gin.Engine {
r := gin.Default()
db := DbInit()
server := handlers.NewServer(db)
router := r.Group("/api")
router.POST("/register", server.Register)
router.POST("/login", server.Login)
return r
}
In main.go
we create a function to initialize our database and create our routers. We create an instance of our database and pass it as an argument to initialize an instance of the server and call our handlers.
func main() {
if err := godotenv.Load(); err != nil {
log.Println("Error loading .env file")
}
port := os.Getenv("PORT")
r := SetupRouter()
log.Fatal(r.Run(":"+port))
}
Here we load our environment variables and initialize the Gin router.
go run github.com/<username>/<module>
Login handler
To develop our login handler we need to write a couple of functions to check the login credentials and verify the password.
user.go
...
func VerifyPassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
The VerifyPassword
function verifies if the password passed by the user matches the hashed password. This function uses The compareHashAndPassword
function from the bcrypt library.
If the password and the hashed password match, then the function continues and generate a token, and returns it.
We create a folder named "utils" in our root directory and create the token.go
file.
token.go
package utils
import (
"fmt"
"github.com/carlosm27/jwtGinApi/models"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"os"
"strconv"
"strings"
"time"
)
func GenerateToken(user model.User) (string, error) {
tokenLifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
if err != nil {
return "", err
}
claims := jwt.MapClaims{}
claims["authorized"] = true
claims["id"] = user.ID
claims["exp"] = time.Now().Add(time.Hour * time.Duration(tokenLifespan)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("API_SECRET")))
}
According to its documentation, MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. This is the default claims type if you don't supply one.
Then, we passed SigningMethodHS256
and the variable claims
to the function NewWithClaims
. This function creates a new Token with the specified signing method and claims, according to the documentation.
And finally return a SignedString
that creates and returns a complete, signed JWT. The token is signed using the SigningMethod specified in the token. We passed to this function an "API_SECRET" or a "SECRETE_KEY".
To generate a key, we have to have installed OpenSSL in our machine, and execute the following command in our terminal.
openssl rand -hex 32
And the output will be something like this:
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
auth.go
func (s *Server)LoginCheck(username, password string) (string, error) {
var err error
user := models.User{}
if err = s.db.Model(models.User{}).Where("username=?", username).Take(&user).Error; err != nil {
return "", err
}
err = models.VerifyPassword(password, user.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
return "", err
}
token, err := utils.GenerateToken(user)
if err != nil {
return "", err
}
return token, nil
}
In the LoginCheck
function we pass a username and a password. The function search in the database to see if the username exists. If it exists, then the function proceeds to verify the password.
...
func (s *Server) Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBind(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := models.User{Username: input.Username, Password: input.Password}
token, err := models.LoginCheck(user.Username, user.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "The username or password is not correct"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}
The Login
function receives a username and a password and binds them to input
for data validation.
Then it creates a User
instance with the data passed and checks the credentials. If the credentials are valid, then it returns a token.
go run github.com/<username>/<module>
Authentication
token.go
func ValidateToken (c *gin.Context) error {
token, err := GetToken(c)
if err != nil {
return err
}
_, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
return nil
}
return errors.New("Invalid token provided")
}
ValidateToken
verify that incoming requests contain a valid token. If there is not a valid token, it will return an error.
func GetToken(c *gin.Context) (*jwt.Token, error) {
tokenString := getTokenFromRequest(c)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return privateKey, nil
})
return token, err
}
GetToken
receives a token and parses it.
The documentation says that the method Parse
parses, validates, verifies the signature, and returns the parsed token. keyFunc
will receive the parsed token and should return the key for validation.
type Keyfunc func(*Token) (interface{}, error)
Keyfunc will be used by the Parse methods as a callback function to supply the key for verification. The function receives the parsed, but unverified Token. This allows you to use properties in the Header of the token (such as
kid
) to identify which key to use.
func getTokenFromRequest(c *gin.Context) string {
bearerToken := c.Request.Header.Get("Authorization")
splitToken := strings.Split(bearerToken, " ")
if len(splitToken) == 2 {
return splitToken[1]
}
return ""
}
Bearer tokens come in the format bearer <JWT>
, so we split them to return a JWT string.
Authorization
middleware.go
package middleware
import (
"fmt"
"github.com/carlosm27/jwtGinApi/utils"
"github.com/gin-gonic/gin"
"net/http"
)
func JwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := utils.ValidateToken(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"Unauthorized":"Authentication required"})
fmt.Println(err)
c.Abort()
return
}
c.Next()
}
}
We create a custom middleware. This handler receives a token and calls ValidateToken()
, if the token is valid, the next handler is called. Otherwise, it will return the status code Unauthorized
.
models/grocery.go
package models
import (
"gorm.io/gorm"
)
type Grocery struct {
gorm.Model
Name string `json: "name"`
Quantity int `json: "quantity"`
}
database.go
package models
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"log"
"os"
)
func Setup() (*gorm.DB, error) {
dbUrl := fmt.Sprint(os.Getenv("DATABASE_URL"))
db, err := gorm.Open(sqlite.Open(dbUrl), &gorm.Config{})
if err != nil {
log.Fatal(err.Error())
}
if err = db.AutoMigrate(&User{}); err != nil {
log.Println(err)
}
if err = db.AutoMigrate(&Grocery{}); err != nil {
log.Println(err)
}
return db, err
}
handlers/grocery.go
package handlers
import (
"github.com/carlosm27/jwtGinApi/models"
"github.com/gin-gonic/gin"
"net/http"
)
type NewGrocery struct {
Name string `json: "name" binding: "required"`
Quantity int `json: "quantity" binding: "required"`
}
func (s *Server) GetGroceries(c *gin.Context) {
var groceries []models.Grocery
if err := s.db.Find(&groceries).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, groceries)
}
func (s *Server) PostGrocery(c *gin.Context) {
var grocery NewGrocery
if err := c.ShouldBindJSON(&grocery); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
newGrocery := models.Grocery{Name: grocery.Name, Quantity: grocery.Quantity}
if err := s.db.Create(&newGrocery).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, newGrocery)
}
In this file are two handlers. One is to get all the groceries in the database. And the other is to add a grocery to the database.
These two handlers will be accessed only by authorized users.
main.go
package main
import (
"github.com/carlosm27/jwtGinApi/models"
"github.com/carlosm27/jwtGinApi/handlers"
"github.com/carlosm27/jwtGinApi/middleware"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"gorm.io/gorm"
"log"
"os"
)
func DbInit() *gorm.DB {
db, err := models.Setup()
if err != nil {
log.Println("Problem setting up database")
}
return db
}
func SetupRouter() *gin.Engine {
r := gin.Default()
db := DbInit()
server := handlers.NewServer(db)
router := r.Group("/api")
router.POST("/register", server.Register)
router.POST("/login", server.Login)
authorized := r.Group("/api/admin")
authorized.Use(middleware.JwtAuthMiddleware())
authorized.GET("/groceries", server.GetGroceries)
authorized.POST("/grocery", server.PostGrocery)
return r
}
We create a router group for our groceries handlers with the JwtAuthMiddleware
handler. This ensures that anyone who wants to get access to GetGroceries
and PostGrocery
has to be authorized and pass through the middleware.
With this approach, any authorized users can see all the groceries in the database, even the groceries add it by other users.
Now, we will add a one-to-many relationship. So, every user only can add and get only their groceries.
Adding one-to-many relationship.
models/user.go
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"-"`
Groceries []Grocery
}
func GetUserById(uid uint) (User, error) {
var user User
db, err := Setup()
if err != nil {
log.Println(err)
return User{}, err
}
if err := db.Preload("Groceries").Where("id=?", uid).Find(&user).Error; err != nil {
return user, errors.New("user not found")
}
return user, nil
}
We add the field Grocery
to the User
. And we add the GetUserById
function. This function will receive an uid
and will search in the database for the user with that uid
.
models/grocery.go
package models
import (
"gorm.io/gorm"
)
type Grocery struct {
gorm.Model
Name string `json: "name"`
Quantity int `json: "quantity"`
UserId uint
}
Here we add the field UserId
, which represents the owner of the grocery.
utils/token.go
...
func CurrentUser(c *gin.Context) (models.User, error) {
err := ValidateToken(c)
if err != nil {
return models.User{}, err
}
token, _ := GetToken(c)
claims, _ := token.Claims.(jwt.MapClaims)
userId := uint(claims["id"].(float64))
user, err := models.GetUserById(userId)
if err != nil {
return models.User{}, err
}
return user, nil
}
CurrentUser
extract the token from the request and validate it, and then extract the "id" of the user from the claims. Then the "id" is passed to the GetUserById
function and returns the user.
handlers/grocery.go
func (s *Server) GetGroceries(c *gin.Context) {
user, err := utils.CurrentUser(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": user.Groceries})
}
Now GetGroceries
only will return the list of the groceries of the user who is making the request.
func (s *Server) PostGrocery(c *gin.Context) {
var grocery models.Grocery
if err := c.ShouldBindJSON(&grocery); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := utils.CurrentUser(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
grocery.UserId = user.ID
if err := s.db.Create(&grocery).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, grocery)
}
When a user wants to add a grocery, only will be added to the list the user owns.
Conclusion
This is the first time I build an authentication app using JWT, and writing this article help me a lot to understand JWT. It was a relief to find enough resources about JWT and how to build an authentication app with Go like the article written by Oluyemi Olususi or the article written by Seef Nasrul, because I was so confused about the pattern, and how the server knows that is an authenticated user who is trying to fetch the data. And those articles help me a lot and the Jwt-Go documentation of course.
You don't have to build your own authentication for every app you build, you may use any alternative of BaaS. But, I did it because I wanted to learn about it. And I learn by building and writing.
I hope you enjoy this article, thank you for taking the time to read it.
If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, or LinkedIn.
The source code is here
Top comments (1)
is it right to do logout like this?
package handlers
import (
"github.com/gin-gonic/gin"
)
func (s *Server) Logout(context *gin.Context) {
context.SetCookie("Mental", "", -1, "", "", false, true)
}
or it is possible to ban the token