Introduction :
Hey there π, In this tutorial, we are going to learn about implementing JWT Authentication in Golang REST-APIs using Fiber Web Framework, PostgreSQL DB and GORM.
JWT :
JSON Web Token (JWT) is a JSON-based open standard (RFC 7519) for creating access tokens that assert some number of claims. For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that it is logged in as admin. The tokens are signed by the server's key, so the client and server are both able to verify that the token is legitimate. This allows the server to trust the token and grants the client the permissions associated with that token.
JWTs are commonly used as a way to authenticate users. When a user logs in to a server, the server creates a JWT that contains information about the user and signs it using a secret key. The server then sends the token back to the client, and the client stores it for future use. When the client wants to access a protected route or resource, it sends the JWT along with the request. The server can then verify the token and grant access to the protected route or resource if the token is valid.
What are we building :
We are going to build a Web API for a User to login, Register, See active User and Logout.
Prerequisitesπ― :
To continue with the tutorial, firstly you need to have Golang, Fiber and PostgreSQL installed. If you've not gone through the previous tutorials on the Fiber Web Framework series you can see them here :)
Installations :
- Golang
- Go-Fiber: We'll see this ahead in the tutorial.
- PostgreSQL
- GORM: We'll see this ahead in the tutorial.
Getting Started π:
Let's get started by creating the main project directory jwt-auth-api by using the following command.
(π₯Be careful, sometimes I've done the explanation by commenting in the code)
mkdir jwt-auth-api //Creates a 'jwt-auth-api' directory
cd jwt-auth-api //Change directory to 'jwt-auth-api'
Now initialize a mod file. (If you publish a module, this must be a path from which your module can be downloaded by Go tools. That would be your code's repository.)
go mod init <repository-name>
In my case repository name is github.com/Siddheshk02/jwt-auth-api
.
To install the Fiber Framework run the following command :
go get -u github.com/gofiber/fiber/v2
To install the Gorm and to install the Gorm Postgres driver, run the following commands resp. :
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
Initializing π»:
Let's set up our server by creating a new instance of Fiber. For this create a file main.go
and add the following code to it :
package main
import (
"github.com/Siddheshk02/jwt-auth-api/routes" // importing the routes package
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
routes.Setup(app) // A routes package/folder is created with 'Setup' function created.
app.Listen(":8000")
}
In the routes.go
, all the endpoints are created for getting user, login, register and logout.
Routes :
Add the following code in the routes.go
file :
package routes
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
api := app.Group("/user")
api.Get("/get-user", func(c *fiber.Ctx) error {
return c.SendString("Hello World!!")
})
}
Now let's run and test the API for the GET endpoint.
Run go run main.go
.
For testing the API, I'm using POSTMAN you can use any tool.
Now, let's add the other endpoints i.e. for login, register and logout.
The routes.go
will look like the following code :
package routes
import (
"github.com/Siddheshk02/jwt-auth-api/controllers" // importing the routes package
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
api := app.Group("/user")
api.Get("/get-user", controllers.User)
api.Post("/register", controllers.Register)
api.Post("/login", controllers.Login)
api.Post("/logout", controllers.Logout)
}
The User, Register, Login and Logout are the functions we are going to create in the controller.go file in the controller package/folder. These are going to perform the actual task.
Firstly, let's try a simple task through the Register function, the following code is added to the Register function.
(For now, You can comment on the other API endpoints i.e. /login
, /get-user
and /logout
)
package controllers
import "github.com/gofiber/fiber/v2"
func Register(c *fiber.Ctx) error {
var data map[string]string
if err := c.BodyParser(&data); err != nil {
return err
}
return c.JSON(data)
}
Now, test the /register
endpoint by passing the data as shown in the image below.
If there is no error then the exact data will be printed.
Database :
Let's create the database and name it as jwt-auth-api
, the steps to create the database are Database>>Create>>Database.
Once the database is created, now make a folder database in which we are going to make dbconn.go
file. In this file, we are going to add the database connection and migration function.
package database
import (
"fmt"
"log"
"github.com/Siddheshk02/jwt-auth-api/models" // this will be imported after you've created the User Model in the models.go file
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
const (
host = "localhost"
port = 5432
user = "postgres"
password = "<password>" //Enter your password for the DB
dbname = "jwt-auth-api"
)
var dsn string = fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
host, port, user, password, dbname)
var DB *gorm.DB
func DBconn() {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
DB = db
db.AutoMigrate(&models.User{}) // we are going to create a models.go file for the User Model.
}
The function gorm.Open()
creates a new connection pool whenever it is called.
db.AutoMigrate()
call helps in creating the table if it is not already present. Database migration is usually things that change the structure of the database over time and this helps in making sure that the database structure is properly migrated to the latest version.
Make a folder models
in which models.go
will be created and updated file with the following code in it.
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string `json:"name"`
Email string `json:"email" gorm:"unique"`
Password []byte `json:"-"`
}
As you can see, our User Model will have a Name, Email, and Password. Here, the Email will be unique. This means, that once we complete our application and try to register new users with the same email, the code wonβt allow us to do it. The best part is that you donβt have to write any code specifically for this. Everything is handled by GORM.
The gorm.Model
specification adds some default properties to the Model, like id, created date, modified date, and deleted date.
Now, Update the main.go
file with the following code.
package main
import (
"github.com/Siddheshk02/jwt-auth-api/database"
"github.com/Siddheshk02/jwt-auth-api/routes"
"github.com/gofiber/fiber/v2"
)
func main() {
database.DBconn()
app := fiber.New()
routes.Setup(app)
app.Listen(":8000")
}
Register :
Let's update the register()
function in the controllers.go
file according to the User Model and database table we've created.
func Register(c *fiber.Ctx) error {
var data map[string]string
if err := c.BodyParser(&data); err != nil {
return err
}
password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14) //GenerateFromPassword returns the bcrypt hash of the password at the given cost i.e. (14 in our case).
user := models.User{
Name: data["name"],
Email: data["email"],
Password: password,
}
database.DB.Create(&user) //Adds the data to the DB
return c.JSON(user)
}
Now, test the endpoint to store the information.
Login :
Let's make the Login work. Before this uncomment the route for the login in the routes.go
file
In the Login we will get the same data string as in Register and we will check the email entered with the database if it is present or not.
If it is present then we will compare the Passwords using an inbuilt function.
func Login(c *fiber.Ctx) error {
var data map[string]string
if err := c.BodyParser(&data); err != nil {
return err
}
var user models.User
database.DB.Where("email = ?", data["email"]).First(&user) //Check the email is present in the DB
if user.ID == 0 { //If the ID return is '0' then there is no such email present in the DB
c.Status(fiber.StatusNotFound)
return c.JSON(fiber.Map{
"message": "user not found",
})
}
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(data["password"])); err != nil {
c.Status(fiber.StatusBadRequest)
return c.JSON(fiber.Map{
"message": "incorrect password",
})
} // If the email is present in the DB then compare the Passwords and if incorrect password then return error.
return c.JSON(user) // If Login is Successfully done return the User data.
}
Test the Login endpoint :
Now we are successfully returning a user but we need to return a JWT Token.
For this, we need a package to be installed.
go get github.com/golang-jwt/jwt
Update the Login()
function wit the following code,
const SecretKey = "secret"
func Login(c *fiber.Ctx) error {
var data map[string]string
if err := c.BodyParser(&data); err != nil {
return err
}
var user models.User
database.DB.Where("email = ?", data["email"]).First(&user) //Check the email is present in the DB
if user.ID == 0 { //If the ID return is '0' then there is no such email present in the DB
c.Status(fiber.StatusNotFound)
return c.JSON(fiber.Map{
"message": "user not found",
})
}
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(data["password"])); err != nil {
c.Status(fiber.StatusBadRequest)
return c.JSON(fiber.Map{
"message": "incorrect password",
})
} // If the email is present in the DB then compare the Passwords and if incorrect password then return error.
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
Issuer: strconv.Itoa(int(user.ID)), //issuer contains the ID of the user.
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), //Adds time to the token i.e. 24 hours.
})
token, err := claims.SignedString([]byte(SecretKey))
if err != nil {
c.Status(fiber.StatusInternalServerError)
return c.JSON(fiber.Map{
"message": "could not login",
})
}
cookie := fiber.Cookie{
Name: "jwt",
Value: token,
Expires: time.Now().Add(time.Hour * 24),
HTTPOnly: true,
} //Creates the cookie to be passed.
c.Cookie(&cookie)
return c.JSON(fiber.Map{
"message": "success",
})
}
NewWithClaims takes two parameters, a signing method and claims. Claims are the actual data that the JWT token will contain.
jwt.NewWithClaims doesn't create the new token, you need to call the SignedString
function passing it the secret key to get the actual JWT token. We stored this token in a cookie.
Now, for cors issue i.e. (the problem which arises when the backend is running on a different port while the front-end is running on a different port), we are using the cors.New() function in the main.go
file.
Update the main.go
file, it will look like the following code
package main
import (
"github.com/Siddheshk02/jwt-auth-api/database"
"github.com/Siddheshk02/jwt-auth-api/routes"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)
func main() {
database.DBconn()
app := fiber.New()
app.Use(cors.New(cors.Config{
AllowCredentials: true, //Very important while using a HTTPonly Cookie, frontend can easily get and return back the cookie.
}))
routes.Setup(app)
app.Listen(":8000")
}
Now, let's test the login endpoint and see the response. Send the request and then click on the Cookies(1)
section.
User :
Before moving ahead, uncomment the Get endpoint for the get-user in the routes.go
file i.e. api.Get("/get-user", controllers.User)
.
Now, let's update the function User()
for getting the logged-in user by using the cookie.
func User(c *fiber.Ctx) error {
cookie := c.Cookies("jwt")
token, err := jwt.ParseWithClaims(cookie, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil //using the SecretKey which was generated in th Login function
})
if err != nil {
c.Status(fiber.StatusUnauthorized)
return c.JSON(fiber.Map{
"message": "unauthenticated",
})
}
claims := token.Claims.(*jwt.StandardClaims)
var user models.User
database.DB.Where("id = ?", claims.Issuer).First(&user)
return c.JSON(user)
}
The jwt.ParseWithClaims
accepts the secret key, it takes a function as the 3rd argument in which you have to return the key.
Test the get-user endpoint.
Here, we can see the password. So, if you don't want to show the password, update the models.go
as,
//Previous : Password []byte `json:"password"`
Password []byte `json:"-"`
Send a request again and you'll not get the password.
Logout :
Before moving ahead, uncomment the Post endpoint for the logout in the routes.go
file i.e. api.Post("/logout", controllers.Logout)
.
Now, let's update the function Logout()
for logging out the present user.
For logging out the user, we need to delete the cookie, but there is no way to remove the cookie in the browser. So, we'll create a different cookie and set its expiry time in the past. Then we'll set the cookie and return the response.
func Logout(c *fiber.Ctx) error {
cookie := fiber.Cookie{
Name: "jwt",
Value: "",
Expires: time.Now().Add(-time.Hour), //Sets the expiry time an hour ago in the past.
HTTPOnly: true,
}
c.Cookie(&cookie)
return c.JSON(fiber.Map{
"message": "success",
})
}
Now, test the logout endpoint.
Now if you test the /get-user endpoint to see the logged-in user, you'll not be able to see any user.
So, there is no logged-in user. If we log-in again, then another cookie will be generated while retrieving the user.
So, this is how JWT Authentication works in golang.
The complete code is saved in this GitHub repository.
Conclusion :
You've Successfully created a REST-API and secured it with JWT Authentication β¨π―
To get more information about Golang concepts, projects, etc. and to stay updated on the Tutorials do follow Siddhesh on Twitter and GitHub.
Until then Keep Learning, Keep Building ππ
Top comments (0)