Posted on • Originally published atcarlosmv.hashnode.dev on
JWT Authentication with Gin | Go
In this article, we will build an authentication API with two endpoints, aregister
endpoint to sign up users and alogin
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.gomodels/ database.go user.go grocery.gomiddleware/ jwtMiddleware.goutils/ token.gomain.go.env
Briefly, I will explain these packages. In themodels
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 thehandlers
module, we have the handlers for registering and login users. Handlers for retrieving and adding groceries.
In themiddleware
module, we have thejwtMiddleware
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 inutils
.
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/gingo get -u gorm.io/gormgo get -u github.com/golang-jwt/jwt/v4go get -u github.com/joho/godotenvgo 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 modelsimport ( "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 alsogorm.Model
adds the fields: ID, CreatedAt, UpdatedAt, and DeletedAt.
databaseSetup.go
package modelsimport ( "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 tosqlite.Open()
.
Register Handler
models/user.go
package modelsimport ( "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 theuser.go
file we add the function to hash the password usingbcrypt
library.
auth.go
package handlersimport ( "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.
Theregister
handler takes the username and password passed by the user and binds them to the variableinput
, if any or some field is missed, it will send aBadRequest
status code.
Then, the username and password are passed to theuser
variable to create a User instance. Then we pass the variableuser
as a reference to thecreate()
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 mainimport ( "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}
Inmain.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))}
TheVerifyPassword
function verifies if the password passed by the user matches the hashed password. This function uses ThecompareHashAndPassword
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 thetoken.go
file.
token.go
package utilsimport ( "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 passedSigningMethodHS256
and the variableclaims
to the functionNewWithClaims
. This function creates a new Token with the specified signing method and claims, according to thedocumentation.
And finally return aSignedString
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 theLoginCheck
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})}
TheLogin
function receives a username and a password and binds them toinput
for data validation.
Then it creates aUser
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 methodParse
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 formatbearer <JWT>
, so we split them to return a JWT string.
Authorization
middleware.go
package middlewareimport ( "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 acustom middleware. This handler receives a token and callsValidateToken()
, if the token is valid, the next handler is called. Otherwise, it will return the status codeUnauthorized
.
models/grocery.go
package modelsimport ( "gorm.io/gorm")type Grocery struct { gorm.Model Name string `json: "name"` Quantity int `json: "quantity"`}
database.go
package modelsimport ( "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 handlersimport ( "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 mainimport ( "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 theJwtAuthMiddleware
handler. This ensures that anyone who wants to get access toGetGroceries
andPostGrocery
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 fieldGrocery
to theUser
. And we add theGetUserById
function. This function will receive anuid
and will search in the database for the user with thatuid
.
models/grocery.go
package modelsimport ( "gorm.io/gorm")type Grocery struct { gorm.Model Name string `json: "name"` Quantity int `json: "quantity"` UserId uint}
Here we add the fieldUserId
, 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 theGetUserById
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})}
NowGetGroceries
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 byOluyemi Olususi or the article written bySeef 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 throughTwitter, orLinkedIn.
The source code ishere
References
Top comments(1)
For further actions, you may consider blocking this person and/orreporting abuse