Introduction
In this article, I will show how to authenticate a user and authorize access to resources in our application. Authenticate and authorize are two different concepts that work together to make applications more secure.
Authentication
Authentication refers to the process where a user enters their email and password and sends the data to the server. The server checks validity of the data and sends a response to the client . In this article, users will be able to sign up and login which will be the authentication part.
Authorization
Authorization refers to the process of checking users credentials after authentication, storing the user credentials in a request token and giving access to resources in the system based on data found in the token part.We will use middleware to give access based on the data in the token.
Token structure
A complete token is made up of three parts namely header, payload and signature.
Header
Identifies the algorithm to be used to generate a signature. In this article we will use the HS256 algorithm.
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Stores information about a user like email, id, role subject and any other information related to the user.
Example
{
"sub": "1234567890",
"name": "john Doe",
"admin": "true"
}
Signature
Validates the token by encoding the header and the payload using the Base64url Encoding RFC and concatenates the two together with a period separator.
Example
HMAC_SHA256(
secret,
base64urlEncoding(header) + '.' +
base64urlEncoding(payload)
)
Prerequisites
- Go runtime installed
- Basic Go knowledge
- PostgreSQL installed
- Code editor of your choice , in this articles we will use Visual Studio Code
Application Structure
In this article the application will be made up of three modules namely controller, middleware and model. The controller module will host code for the Gin Web framework and route HTTP requests from the client to the server. The middleware will host code for authorizing HTTP requests coming from the client. The model module will host code for PostgreSQL database connection and all database operations.
Navigate to your working directory add the following commands in the terminal to set up our application.
Create application directory
mkdir jwt-demo && cd jwt-demo
Create modules
mkdir controller middleware model
Create main.go
and .env
file
touch main.go .env
Initialize go module
go mod init example.com/jwt-demo
Install dependencies
go get -u github.com/gin-gonic/gingo get -u gorm.io/gormgo get -u gorm.io/driver/postgresgo get -u github.com/golang-jwt/jwt/v4 go get github.com/joho/godotenv
To start us off, we need to define environmental variables such as database port, host and password. Add the below code in the .env
file.
SECRET=topsecret DB_HOST=localhost DB_PORT=5432 DB_USER=postgres DB_NAME=postgres DB_PASSWORD=mypassword
Please note that these environmental variables will not be the same as your credentials, especially the DB_USER ,DB_NAME and DB_PASSWORD. Replace these with your own values that you use to access your postgres database.
Next thing to do is to add code for database operations and user model in the database.go file.
Navigate into the database module and create a database.go file
cd model && touch database.go
model/database.go
package model
import (
"fmt"
"log"
"os"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/joho/godotenv"
"golang.org/x/crypto/bcrypt"
)
var DB *gorm.DB
var err error
var secretKey = envVariable("SECRET")
// ############ Application DB ###########################
func SetDBClient() {
var (
host = envVariable("DB_HOST")
port = envVariable("DB_PORT")
user = envVariable("DB_USER")
dbname = envVariable("DB_NAME")
password = envVariable("DB_PASSWORD")
)
dns := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
host,
port,
user,
dbname,
password,
)
DB, err = gorm.Open("postgres", dns)
DB.AutoMigrate(User{})
if err != nil {
fmt.Println(err)
}
fmt.Println("Connection to the database is successful")
}
func envVariable(key string) string {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file")
}
return os.Getenv(key)
}
// ################ Application Model ####################
type User struct {
Email string `json:"email" gorm:"unique"`
Password string `json:"password"`
}
func (u *User) GeneratePasswordHarsh() error {
bytes, err := bcrypt.GenerateFromPassword([]byte(u.Password), 14)
u.Password = string(bytes)
return err
}
func (u *User) CheckPasswordHarsh(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
In this file , we start by importing different packages for database interaction and managing user data.
github.com/jinzhu/gorm
Database object relational mapper for postgres operations.github.com/jinzhu/gorm/dialects/postgres
: Gives us access to database operations such as creating a database instancegithub.com/joho/godotenv
: For accessing environment variables in the .env filegolang.org/x/crypto/bcrypt
: For password hashing when users sign up.
We start by declaring an instance of a database using the var DB *gorm.DB
. This will be assigned a database client after a successful database client creation. We also declare an error variable that gets assigned a value when an error occurs while working with the database client. Lastly , we define a secret variable that stores the secret key defined in the .env file.
Next we define a User struct with email and password fields. The User struct has methods for generating a password harsh using the GenerataPasswordharsh()
function. It also has the CheckPasswordHash()
function that validates harshed passwords.
We also have the SetDBClient()
function that creates a new database client using environmental variables from the .env file. The SetDBClient()
function starts with a capital letter S meaning that its an exported function that will be used in the main.go file.The SetDBClient()
function uses the envVariable() function to get environmental variables from the .env
file.
controller/gin.go
The main responsibility of the code in this file is to route POST and GET requests to their respective handler functions. This file host three handler functions namely:
<b>Signup()</b>
, for creating a new user<b>Login()</b>
for authenticating and authorizing access to the application.<b>Resources() </b>
a function that returns registered users in the application as resources. Only authenticated and authorized users can get access to this resource
These function takes as an argument a gin context c *gin.Context
.The context is used HTTP requests operations such getting data, returning responses status code to the client.
This file starts off by importing the model
, gin
and jwt
for database interaction, HTTP routing and token creating respectively.
Signup() handler function
package controller
import (
"net/http"
"os"
"time"
"example.com/jwt-demo/model"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
)
func Signup(c *gin.Context) {
var reqUser model.User
// Bind user data
if err := c.ShouldBind(&reqUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
// Check user exists in db
var dbUser model.User
model.DB.Where("email =?", reqUser.Email).First(&dbUser)
if dbUser.Email != "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "user with email found, login",
})
return
}
// Harsh user password
err := reqUser.GeneratePasswordHarsh()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "unable to harsh password",
})
return
}
// Add user into the database
res := model.DB.Create(&reqUser)
if res.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to create user",
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": reqUser,
})
}
The signup handler function performs the following operations.
- Binds data from the client with an existing user model. If the binding is not successful , an error is returned to the client using the gin context.
- Check if a user exists. After a successful binding, we check if a user with the email provided exists in the database. If a user exists, we return to the client a message indicating a user already exists.
- Generate password harsh. If no user is found with the email from the client, we use the
GenerataPassrdHarsh()
method from the model.User struct to generate a harshed password and assign the password to the password field of the model.User struct. - Save user into the database. After a successful password harshing, the new user is saved into the database using the res := models.DB.Create() statement.
Please note in the signup operation, there is no interaction with the JWT tokens because we are only registering a user hence no token required. Login section is of interest to use. This is where we generate a token.
Login handler function
The login function is responsible for creating tokens for users who have already registered.
func Login(c *gin.Context) {
var reqUser model.User
if err := c.ShouldBind(&reqUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err,
})
return
}
// Get user from model
var dbUser model.User
model.DB.Where("email =?", reqUser.Email).First(&dbUser)
if dbUser.Email == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid email or password",
})
return
}
if dbUser.CheckPasswordHarsh(reqUser.Password) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": dbUser.Email,
"exp": time.Now().Add(time.Minute * 10).Unix(),
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("Authorization", tokenString, 3600*24*30, "", "", false, true)
c.JSON(http.StatusOK, gin.H{
"user": dbUser,
})
} else {
c.JSON(http.StatusNotFound, gin.H{
"error": "Invalid email or password",
})
}
}
Below are the steps authenticating and authorizing a user.
- Binds data from the client with an existing user model. If the binding is not successful , an error is returned to the client using the gin context.
- Check if a user exists. After a successful binding, we check if a user with the email provided exists in the database. If a user exists, we move to the next step to validate the harshe password.
- Validate password using the CheckPasswordHarsh() function. This is the main focus of our tutorial. We generate tokens using the JWT package. The jwt package has the
NewWithClaims()
function that takes in a signing method in this case we are usingHS256
and a claim as the second argument. Claims are statements about an entity in this case the use email and expiration date for the token. For more information about claims read here.TheNewWithClaims()
function returns a token to the caller.After a successful token creation, we sign the token using the secret key from the.env
file using the statementtokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
. - Set cookies using gin context. The generated token gets set in the response headers using the
SetCookie()
function. The statementc.SetCookie("Authorization", tokenString, 3600*24*30, "", "", false, true)
create a cookie of type Authorization and use the generated tokenString to authorize it.After a successful login, the user details are returned to the client.
Resources handler function
This function returns all the users in the application to the client. Later on , we will add a middleware that will allow users with valid tokens to access this resource.
func Resources(c *gin.Context) {
var users []model.User
res := model.DB.Find(&users)
if res.Error != nil {
c.JSON(http.StatusOK, gin.H{
"message": "error fetching users",
})
return
}
c.JSON(http.StatusOK, gin.H{
"users": users,
})
}
middleware/authorization.go
This file hosts code for validating a token from the client. Being a middleware, HTTP requests will have to pass through this middleware and return to the respective handler function only if the token validates successfully , otherwise a response not authorized will be returned to the client. In the middleware module , add the below code.
package middleware
import (
"fmt"
"net/http"
"os"
"time"
"example.com/jwt-demo/model"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
)
func Authorize(c *gin.Context) {
tokenString, err := c.Cookie("Authorization")
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
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["sub"])
}
return []byte(os.Getenv("SECRET")), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if float64(time.Now().Unix()) > claims["exp"].(float64) {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
var user model.User
model.DB.Where("email =?", claims["sub"]).First(&user)
if user.Email == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Set("user", user)
c.Next()
} else {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
The Authorize()
middleware takes as an argument the gin context. The context is used to access the token using the <b>tokenString, err := c.Cookie("Authorization") </b>
statement.If the token is missing in the cookie, we abort the operation and return to the user a HTTP response with an not authorized status code.
If the token exists, we parse it using the token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error)
. The parsing step also confirms that we are using the correct signing method, if a different signing method is found, we return an error to the client. If the correct signing method is found, we return our secret key from the .env file as bytes using return []byte(os.Getenv("SECRET")), nil
statement.
The next step is to check the validity of the token by checking the data in the claims. If the token is valid, we ensure that the token has not expired. If the token is not expired we pull the email from the claim using the claims[“sub”]
statement. The email is then used to pull the user from the database with the found email from the claim. If a user is found with an email, we abort the operation and return an authorized response to the client.
After successfully getting a user from the database using the email, we set the user into the context using the c.Set("user", user)
and move the control to the next handler function using the c.Next()
function.
Now we have the database, middleware and controller ready, we now pull together our code in the main.go file. This is the main entry point to our application.
main.go
package main
import (
"fmt"
"net/http"
"example.com/jwt-demo/controller"
"example.com/jwt-demo/middleware"
"example.com/jwt-demo/model"
"github.com/gin-gonic/gin"
)
func init() {
model.SetDBClient()
}
func main() {
fmt.Println("Welcome to Go authorization with Go")
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Home router",
})
})
r.POST("/signup", controller.Signup)
r.POST("/login", controller.Login)
r.GET("/api/v1", middleware.Authorize, controller.Resources)
r.Run(":5000")
}
In the main.go file, we initialize a gin router using the r := gin.Default() which returns a gin engine. We use the gin router to expose the POST and GET methods. The Signup and Login methods use the POST HTTP method and route requests to the controller.Signup and controller.Login handler respectively.
Testing
To test the JWT token, we will use Postman to signup and login into our application.
We first need to run our application. In your terminal move to the root folder where main.go
file is and run the below command
Example
$ go run main.go
Connection to the database is successful
Welcome to Go authorization with Go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] POST /signup --> example.com/jwt-demo/controller.Signup (3 handlers)
[GIN-debug] POST /login --> example.com/jwt-demo/controller.Login (3 handlers)
[GIN-debug] GET /api/v1 --> example.com/jwt-demo/controller.Resources (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000
Signup user
Login User
Get resource with token
After making this request, if you clear your session and cookies, you will not be able to access data from the resources route.
Get resources without token
Summary
In this article, we learn about authentication and authorization using JWT. We learn about how to sign up a user, login a user and generate a token for the users and later on access resources only if a user has a token.
References
https://jwt.io
https://github.com/golang-jwt/jwt