This article will teach you how to implement RS256 JWT Authentication and Authorization with Golang, Gin Gonic, SQLC, PostgreSQL, and Docker-compose. You will also learn how to use asymmetric encryption (private and public keys) to generate access and refresh tokens in Golang.

Golang, Gin Gonic & PostgreSQL API Series:

  1. API with Golang, PostgreSQL, SQLC & Gin Gonic: Project Setup
  2. Golang, SQLC, and PostgreSQL: JWT Access & Refresh Tokens
  3. Golang CRUD RESTful API with SQLC and PostgreSQL

Related articles:

Build Golang & PostgreSQL API JWT Access and Refresh Tokens
Golang & PostgreSQL API

Note: Read the Golang & PostgreSQL setup article before implementing the JWT authentication in this article.

Golang & PostgreSQL JWT Authentication Overview

You can import the Postman collection used in testing the Golang PostgreSQL JWT authentication and authorization into your own Postman to make your life easier.

-Register a new user

golang mongodb jwt authentication register new user

-Sign in to your account with your credentials

golang mongodb jwt authentication login the registered user

The Golang Gin server will return some cookies to the client or user’s browser after the credentials have been validated.

Click the Cookies tab in Postman to view the cookies returned by the Gin server after the user was authenticated.

golang mongodb jwt authentication view the cookies in postman

-Retrieve the authenticated user’s credentials

golang mongodb jwt authentication get the signed in user

-Request a new access token when it expires

golang mongodb jwt authentication refresh the expired access token

-Logout the authenticated user

golang mongodb jwt authentication logout the user

JWT Authentication with Golang and PostgreSQL example

With this Golang, Gin, PostgreSQL JSON Web Token authentication example, the user will be able to do the following:

  • Register for a new account with the required credentials
  • Login with the registered credentials
  • Request a new access token when it expires
  • Retrieve the profile information with the cookies sent by the Gin server.
RESOURCEHTTP METHODROUTEDESCRIPTION
usersGET/api/users/meGet the signed-in user’s credentials
authPOST/api/auth/registerRegister a new user
authPOST/api/auth/loginSign in the user
authGET/api/auth/refreshRequest a new access token
authGET/api/auth/logoutLogout user

Generate the Token Public and Private Keys

I already added the private and public keys to the app.env file but you can follow the steps below to generate them yourself.

Step 1: Go to this website and click on the “Generate New Keys” button to generate the private and public keys for the JWT tokens.

Step 2: Copy the generated private key and navigate to this website to convert it to base64. Later, we’ll decode them back to ASCII strings using the Golang package.

Step 3: Paste the copied private key into the HTML Textarea field on the Base64 Decode and Encode website and click on the “Encode” button to convert it to “base64“.

Step 4: Copy the base64 encoded private key and add it to the app.env file asACCESS_TOKEN_PRIVATE_KEY.

Step 5: Navigate back to the website where you generated the keys and copy the corresponding public key.

Step 6: Go back to the Base64 Decode and Encode website and convert it to base64 before adding it to the app.env file as ACCESS_TOKEN_PUBLIC_KEY .

Step 7: Repeat the above process for the refresh token.

Update Environment Variables

In case you were having difficulties in encoding the private and public keys, you can copy and paste the environment variables below into your app.env file to make your life easier.

app.env


PORT=8000
NODE_ENV=development

POSTGRES_DRIVER=postgres
POSTGRES_SOURCE=postgresql://admin:password123@localhost:6500/golang_postgres?sslmode=disable

POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=golang_postgres

ORIGIN=http://localhost:3000

ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCUEFJQkFBSkJBTzVIKytVM0xrWC91SlRvRHhWN01CUURXSTdGU0l0VXNjbGFFKzlaUUg5Q2VpOGIxcUVmCnJxR0hSVDVWUis4c3UxVWtCUVpZTER3MnN3RTVWbjg5c0ZVQ0F3RUFBUUpCQUw4ZjRBMUlDSWEvQ2ZmdWR3TGMKNzRCdCtwOXg0TEZaZXMwdHdtV3Vha3hub3NaV0w4eVpSTUJpRmI4a25VL0hwb3piTnNxMmN1ZU9wKzVWdGRXNApiTlVDSVFENm9JdWxqcHdrZTFGY1VPaldnaXRQSjNnbFBma3NHVFBhdFYwYnJJVVI5d0loQVBOanJ1enB4ckhsCkUxRmJxeGtUNFZ5bWhCOU1HazU0Wk1jWnVjSmZOcjBUQWlFQWhML3UxOVZPdlVBWVd6Wjc3Y3JxMTdWSFBTcXoKUlhsZjd2TnJpdEg1ZGdjQ0lRRHR5QmFPdUxuNDlIOFIvZ2ZEZ1V1cjg3YWl5UHZ1YStxeEpXMzQrb0tFNXdJZwpQbG1KYXZsbW9jUG4rTkVRdGhLcTZuZFVYRGpXTTlTbktQQTVlUDZSUEs0PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==

ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBTzVIKytVM0xrWC91SlRvRHhWN01CUURXSTdGU0l0VQpzY2xhRSs5WlFIOUNlaThiMXFFZnJxR0hSVDVWUis4c3UxVWtCUVpZTER3MnN3RTVWbjg5c0ZVQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
ACCESS_TOKEN_EXPIRED_IN=15m
ACCESS_TOKEN_MAXAGE=15


REFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkJBSWFJcXZXeldCSndnYjR1SEhFQ01RdHFZMTI5b2F5RzVZMGlGcG51a0J1VHpRZVlQWkE4Cmx4OC9lTUh3Rys1MlJGR3VxMmE2N084d2s3TDR5dnY5dVY4Q0F3RUFBUUpBRUZ6aEJqOUk3LzAxR285N01CZUgKSlk5TUJLUEMzVHdQQVdwcSswL3p3UmE2ZkZtbXQ5NXNrN21qT3czRzNEZ3M5T2RTeWdsbTlVdndNWXh6SXFERAplUUloQVA5UStrMTBQbGxNd2ZJbDZtdjdTMFRYOGJDUlRaZVI1ZFZZb3FTeW40YmpBaUVBaHVUa2JtZ1NobFlZCnRyclNWZjN0QWZJcWNVUjZ3aDdMOXR5MVlvalZVRlVDSUhzOENlVHkwOWxrbkVTV0dvV09ZUEZVemhyc3Q2Z08KU3dKa2F2VFdKdndEQWlBdWhnVU8yeEFBaXZNdEdwUHVtb3hDam8zNjBMNXg4d012bWdGcEFYNW9uUUlnQzEvSwpNWG1heWtsaFRDeWtXRnpHMHBMWVdkNGRGdTI5M1M2ZUxJUlNIS009Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t

REFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSWFJcXZXeldCSndnYjR1SEhFQ01RdHFZMTI5b2F5Rwo1WTBpRnBudWtCdVR6UWVZUFpBOGx4OC9lTUh3Rys1MlJGR3VxMmE2N084d2s3TDR5dnY5dVY4Q0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==

REFRESH_TOKEN_EXPIRED_IN=60m
REFRESH_TOKEN_MAXAGE=60

Now create a config/default.go file and add the following configurations to enable Viper to load the environment variables and make them available in the application.

config/default.go


package config

import (
	"time"

	"github.com/spf13/viper"
)

type Config struct {
	PostgreDriver  string `mapstructure:"POSTGRES_DRIVER"`
	PostgresSource string `mapstructure:"POSTGRES_SOURCE"`

	Port string `mapstructure:"PORT"`

	Origin string `mapstructure:"ORIGIN"`

	AccessTokenPrivateKey  string        `mapstructure:"ACCESS_TOKEN_PRIVATE_KEY"`
	AccessTokenPublicKey   string        `mapstructure:"ACCESS_TOKEN_PUBLIC_KEY"`
	RefreshTokenPrivateKey string        `mapstructure:"REFRESH_TOKEN_PRIVATE_KEY"`
	RefreshTokenPublicKey  string        `mapstructure:"REFRESH_TOKEN_PUBLIC_KEY"`
	AccessTokenExpiresIn   time.Duration `mapstructure:"ACCESS_TOKEN_EXPIRED_IN"`
	RefreshTokenExpiresIn  time.Duration `mapstructure:"REFRESH_TOKEN_EXPIRED_IN"`
	AccessTokenMaxAge      int           `mapstructure:"ACCESS_TOKEN_MAXAGE"`
	RefreshTokenMaxAge     int           `mapstructure:"REFRESH_TOKEN_MAXAGE"`
}

func LoadConfig(path string) (config Config, err error) {
	viper.AddConfigPath(path)
	viper.SetConfigType("env")
	viper.SetConfigName("app")

	viper.AutomaticEnv()

	err = viper.ReadInConfig()
	if err != nil {
		return
	}

	err = viper.Unmarshal(&config)
	return
}


Define User models with structs

We already have the Golang structs and CRUD services generated by SQLC but we also need to create some custom structs to help us sign in the user and filter the data returned by the PostgreSQL database before sending a response to the user.

To define a struct with JSON tags, you need to specify the field name followed by the field type and optional tags which will be used by Gin Gonic for validation, marshaling, and unmarshaling.

Now create a SignInInput struct to specify the fields required to sign in the registered user.

The json and binding tags will be used by Gin Gonic for validating the request body to ensure the user provided the required information.

Lastly, create a FilteredResponse() function to filter the data returned by Postgres to avoid sending sensitive information to the user.

models/user.model.go


package models

import (
	"time"

	db "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
)

// ? SignInInput struct
type SignInInput struct {
	Email    string `json:"email" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// ? UserResponse struct
type UserResponse struct {
	ID        string    `json:"id,omitempty"`
	Name      string    `json:"name,omitempty"`
	Email     string    `json:"email,omitempty"`
	Role      string    `json:"role,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

func FilteredResponse(user db.User) UserResponse {
	return UserResponse{
		ID:        user.ID.String(),
		Email:     user.Email,
		Name:      user.Name,
		Role:      user.Role,
		CreatedAt: user.CreatedAt,
		UpdatedAt: user.UpdatedAt,
	}
}


Create Helper functions to hash and verify password

One essential step of the authentication flow is to hash the plain-text password provided by the user before saving the document to the database.

To hash the plain-text password, we use a hashing algorithm to transform it into a hashed string of characters.

Hashing the password will make it difficult for a hacker to retrieve the original plain-text password when the database is compromised.

In brief, when two strings are hashed the results are the same so we use salt to ensure that in the situation where two users provide the same password, they will not end up with the same hashed password.

There are different ways of hashing a plain-text password but in this article, I will use the Golang Bcrypt package to hash the passwords before saving them to the database.

To hash the plain-text password with Bcrypt, we specify a Cost Factor which is the amount of time required to calculate a single hash and the actual string to hash.

The higher the Cost Factor the longer the hashing time and the more difficult it is to brute force. Nowadays, we have powerful high computing computers so using a Cost Factor of 12 should be okay.

Now, let’s create two utility functions to hash and verify the user’s password.

utils/password.go


package utils

import "golang.org/x/crypto/bcrypt"

func HashPassword(password string) string {
	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	return string(hashedPassword)
}

func ComparePassword(hashedPassword string, candidatePassword string) error {
	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(candidatePassword))
}

Utility functions to sign and verify JWT tokens

User authentication and authorization can be achieved with different strategies but each strategy has security flaws.

Also, the concept of authentication can really get out of hand so libraries like PassportJs and NextAuth provide different strategies to simplify the process.

In this article, we will use JSON Web Tokens to authenticate and persist the user to allow them to access protected routes without having to sign in on every request.

Create Json Web Tokens

With JSON Web Tokens, we can add stateless authentication to our Golang server but that alone is not enough to secure our application since the only way the tokens are invalidated is when they expire.

You can learn more about the drawbacks of JSON Web Tokens in this article.

Now let’s create a CreateToken() helper function to generate access and refresh tokens with the RS256 algorithm.

utils/token.go


package utils

import (
	"encoding/base64"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt"
)

func CreateToken(ttl time.Duration, payload interface{}, privateKey string) (string, error) {
	decodedPrivateKey, err := base64.StdEncoding.DecodeString(privateKey)
	if err != nil {
		return "", fmt.Errorf("could not decode key: %w", err)
	}
	key, err := jwt.ParseRSAPrivateKeyFromPEM(decodedPrivateKey)

	if err != nil {
		return "", fmt.Errorf("create: parse key: %w", err)
	}

	now := time.Now().UTC()

	claims := make(jwt.MapClaims)
	claims["sub"] = payload
	claims["exp"] = now.Add(ttl).Unix()
	claims["iat"] = now.Unix()
	claims["nbf"] = now.Unix()

	token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key)

	if err != nil {
		return "", fmt.Errorf("create: sign token: %w", err)
	}

	return token, nil
}

Let me explain what we did above:

  • First and foremost we decoded the base64 private keys back to ASCII strings.
  • Next, we parsed the decoded private keys and created the token claims.
  • Lastly, we signed the token with the RS256 algorithm and the token claims before returning it.

Verify JSON Web Tokens

Now let’s create a ValidateToken() function to verify the access and refresh tokens.

The ValidateToken() validates the token and returns the token payload or an error if the token was manipulated or has expired.

utils/token.go


func ValidateToken(token string, publicKey string) (interface{}, error) {
	decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey)
	if err != nil {
		return nil, fmt.Errorf("could not decode: %w", err)
	}

	key, err := jwt.ParseRSAPublicKeyFromPEM(decodedPublicKey)

	if err != nil {
		return "", fmt.Errorf("validate: parse key: %w", err)
	}

	parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
		if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("unexpected method: %s", t.Header["alg"])
		}
		return key, nil
	})

	if err != nil {
		return nil, fmt.Errorf("validate: %w", err)
	}

	claims, ok := parsedToken.Claims.(jwt.MapClaims)
	if !ok || !parsedToken.Valid {
		return nil, fmt.Errorf("validate: invalid token")
	}

	return claims["sub"], nil
}


Create the Authentication Controllers

Now let’s create an AuthController struct to have access to the *db.Queries object and the context interface using a popular programming paradigm called composition.

Signup user controller

controllers/auth.controller.go


package controllers

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/wpcodevo/golang-postgresql-api/config"
	db "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
	"github.com/wpcodevo/golang-postgresql-api/models"
	"github.com/wpcodevo/golang-postgresql-api/utils"
)

type AuthController struct {
	db  *db.Queries
	ctx context.Context
}

func NewAuthController(db *db.Queries, ctx context.Context) *AuthController {
	return &AuthController{db, ctx}
}

func (ac *AuthController) SignUpUser(ctx *gin.Context) {
	var credentials *db.CreateUserParams

	if err := ctx.ShouldBindJSON(&credentials); err != nil {
		ctx.JSON(http.StatusBadRequest, err.Error())
		return
	}

	hashedPassword := utils.HashPassword(credentials.Password)

	args := &db.CreateUserParams{
		Name:      credentials.Name,
		Email:     credentials.Email,
		Password:  hashedPassword,
		Photo:     "default.jpeg",
		Verified:  true,
		Role:      "user",
		UpdatedAt: time.Now(),
	}

	user, err := ac.db.CreateUser(ctx, *args)

	if err != nil {
		ctx.JSON(http.StatusBadGateway, err.Error())
		return
	}

	userResponse := models.FilteredResponse(user)

	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "data": gin.H{"user": userResponse}})
}

Details of what we did in the SignUpUser receiver function:

  • First, we validated the user’s input against the *db.CreateUserParams struct generated by SQLC and returned the appropriate validation error to the user.
  • Next, we hashed the user’s plain-text password with the HashPassword() utility function we defined above.
  • Then we declared the db.CreateUserParams arguments with the required fields before passing it to the “CreateUser” service generated by SQLC to add the user to the database.
  • Lastly, we filtered the data returned by the Postgres database before returning the JSON response to the user. This enables us to omit sensitive information like the user’s password from the JSON response.

Login user controller

Now that we are able to create a new user, let’s define a controller to log in the registered user.

controllers/auth.controller.go


func (ac *AuthController) SignInUser(ctx *gin.Context) {
	var credentials *models.SignInInput

	if err := ctx.ShouldBindJSON(&credentials); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	user, err := ac.db.GetUserByEmail(ac.ctx, credentials.Email)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or password"})
		return
	}

	if err := utils.ComparePassword(user.Password, credentials.Password); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or password"})
		return
	}

	config, _ := config.LoadConfig(".")

	// Generate Tokens
	access_token, err := utils.CreateToken(config.AccessTokenExpiresIn, user.ID, config.AccessTokenPrivateKey)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	refresh_token, err := utils.CreateToken(config.RefreshTokenExpiresIn, user.ID, config.RefreshTokenPrivateKey)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.SetCookie("access_token", access_token, config.AccessTokenMaxAge*60, "/", "localhost", false, true)
	ctx.SetCookie("refresh_token", refresh_token, config.RefreshTokenMaxAge*60, "/", "localhost", false, true)
	ctx.SetCookie("logged_in", "true", config.AccessTokenMaxAge*60, "/", "localhost", false, false)

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "access_token": access_token})
}

In the SignInUser receiver function, we validated the request body against the *models.SignInInput struct we defined above and called the GetUserByEmail() service generated by SQLC to check if that user exists in the database.

Next, we called the ComparePassword() helper function to validate the plain-text password against the hashed password stored in the database.

Assuming there weren’t any errors, we called the CreateToken() utility function to generate both the access and refresh tokens.

Lastly, we return the access and refresh tokens as HTTPOnly cookies to the user’s client or browser.

Refresh access token controller

With this authentication flow, the access token can be refreshed after every 15 minutes as long as the refresh token is valid. This approach has some security flaws so later we’ll integrate Redis to add an extra layer of security.

controllers/auth.controller.go


func (ac *AuthController) RefreshAccessToken(ctx *gin.Context) {
	message := "could not refresh access token"

	cookie, err := ctx.Cookie("refresh_token")

	if err != nil {
		ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": message})
		return
	}

	config, _ := config.LoadConfig(".")

	sub, err := utils.ValidateToken(cookie, config.RefreshTokenPublicKey)
	if err != nil {
		ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	user, err := ac.db.GetUserById(ac.ctx, uuid.MustParse(fmt.Sprint(sub)))
	if err != nil {
		ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": "the user belonging to this token no logger exists"})
		return
	}

	access_token, err := utils.CreateToken(config.AccessTokenExpiresIn, user.ID, config.AccessTokenPrivateKey)
	if err != nil {
		ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	ctx.SetCookie("access_token", access_token, config.AccessTokenMaxAge*60, "/", "localhost", false, true)
	ctx.SetCookie("logged_in", "true", config.AccessTokenMaxAge*60, "/", "localhost", false, false)

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "access_token": access_token})
}

Logout user controller

The logout controller is the easiest to implement. The LogoutUser handler will be called to send expired cookies to the user’s browser or client to log them out.

controllers/auth.controller.go


func (ac *AuthController) LogoutUser(ctx *gin.Context) {
	ctx.SetCookie("access_token", "", -1, "/", "localhost", false, true)
	ctx.SetCookie("refresh_token", "", -1, "/", "localhost", false, true)
	ctx.SetCookie("logged_in", "", -1, "/", "localhost", false, true)

	ctx.JSON(http.StatusOK, gin.H{"status": "success"})
}

Authentication middleware

We are now ready to create a middleware to authenticate the user since we have all our controllers in place. Let’s create a DeserializeUser middleware to validate the access token retrieved from either the cookies object or Authorization header.

After retrieving the access token, we will call the ValidateToken() helper function to validate it and extract the payload we stored in it.

Next, we will use the user’s ID we stored as the payload to check if that user still exists in the database and attach the returned user to the Gin context struct with a currentUser key.

middleware/deserialize-user.go


package middleware

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/wpcodevo/golang-postgresql-api/config"
	db "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
	"github.com/wpcodevo/golang-postgresql-api/utils"
)

func DeserializeUser(db *db.Queries) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		var access_token string
		cookie, err := ctx.Cookie("access_token")

		authorizationHeader := ctx.Request.Header.Get("Authorization")
		fields := strings.Fields(authorizationHeader)

		if len(fields) != 0 && fields[0] == "Bearer" {
			access_token = fields[1]
		} else if err == nil {
			access_token = cookie
		}

		if access_token == "" {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": "You are not logged in"})
			return
		}

		config, _ := config.LoadConfig(".")
		sub, err := utils.ValidateToken(access_token, config.AccessTokenPublicKey)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": err.Error()})
			return
		}

		user, err := db.GetUserById(context.TODO(), uuid.MustParse(fmt.Sprint(sub)))
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": "fail", "message": "The user belonging to this token no logger exists"})
			return
		}

		ctx.Set("currentUser", user)
		ctx.Next()
	}
}


Create the User controller

GetMe Controller

Here we will extract the user object we stored in the Gin context struct using the MustGet method and return it to the user as a JSON response.

controllers/user.controller.go


type UserController struct {
	userService services.UserService
}

func NewUserController(userService services.UserService) UserController {
	return UserController{userService}
}

func (uc *UserController) GetMe(ctx *gin.Context) {
	currentUser := ctx.MustGet("currentUser").(*models.DBResponse)

	ctx.JSON(http.StatusOK, gin.H{"status": "success", "data": gin.H{"user": models.FilteredResponse(currentUser)}})
}

Create routes

The routes will be responsible for routing the request to the appropriate controller. I believe you are already familiar with this syntax if you’ve worked with Express, Fastify, and FastAPI.

Auth Routes

routes/auth.routes.go


package routes

import (
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-postgresql-api/controllers"
	db "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
	"github.com/wpcodevo/golang-postgresql-api/middleware"
)

type AuthRoutes struct {
	authController controllers.AuthController
	db             *db.Queries
}

func NewAuthRoutes(authController controllers.AuthController, db *db.Queries) AuthRoutes {
	return AuthRoutes{authController, db}
}

func (rc *AuthRoutes) AuthRoute(rg *gin.RouterGroup) {

	router := rg.Group("/auth")
	router.POST("/register", rc.authController.SignUpUser)
	router.POST("/login", rc.authController.SignInUser)
	router.GET("/refresh", rc.authController.RefreshAccessToken)
	router.GET("/logout", middleware.DeserializeUser(rc.db), rc.authController.LogoutUser)
}


User Routes

routes/user.routes.go


package routes

import (
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-postgresql-api/controllers"
	db "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
	"github.com/wpcodevo/golang-postgresql-api/middleware"
)

type UserRoutes struct {
	userController controllers.UserController
	db             *db.Queries
}

func NewUserRoutes(userController controllers.UserController, db *db.Queries) UserRoutes {
	return UserRoutes{userController, db}
}

func (rc *UserRoutes) UserRoute(rg *gin.RouterGroup) {

	router := rg.Group("/users")
	router.GET("/me", middleware.DeserializeUser(rc.db), rc.userController.GetMe)
}


Update the main file

Run this command to install the CORS package to enable us to configure the Gin server to accept requests from cross-origin domains.


go get github.com/gin-contrib/cors

cmd/server/main.go


package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-postgresql-api/config"
	"github.com/wpcodevo/golang-postgresql-api/controllers"
	dbConn "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
	"github.com/wpcodevo/golang-postgresql-api/routes"

	_ "github.com/lib/pq"
)

var (
	server *gin.Engine
	db     *dbConn.Queries
	ctx    context.Context

	AuthController controllers.AuthController
	UserController controllers.UserController
	AuthRoutes     routes.AuthRoutes
	UserRoutes     routes.UserRoutes
)

Create an init function to:

  1. Connect to PostgreSQL database
  2. Instantiate the constructors of the controllers, and routes.

cmd/server/main.go


func init() {
	ctx = context.TODO()
	config, err := config.LoadConfig(".")

	if err != nil {
		log.Fatalf("could not load config: %v", err)
	}

	conn, err := sql.Open(config.PostgreDriver, config.PostgresSource)
	if err != nil {
		log.Fatalf("could not connect to postgres database: %v", err)
	}

	db = dbConn.New(conn)

	fmt.Println("PostgreSQL connected successfully...")

	AuthController = *controllers.NewAuthController(db, ctx)
	UserController = controllers.NewUserController(db, ctx)
	AuthRoutes = routes.NewAuthRoutes(AuthController, db)
	UserRoutes = routes.NewUserRoutes(UserController, db)

	server = gin.Default()
}

Next, create a main function to configure and start the Gin server. Also, we need to add the CORS configurations to the middleware stack if you’ll be making requests from a cross-origin domain.

cmd/server/main.go


func main() {
	config, err := config.LoadConfig(".")

	if err != nil {
		log.Fatalf("could not load config: %v", err)
	}

	corsConfig := cors.DefaultConfig()
	corsConfig.AllowOrigins = []string{config.Origin}
	corsConfig.AllowCredentials = true

	server.Use(cors.New(corsConfig))

	router := server.Group("/api")

	router.GET("/healthchecker", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": "Welcome to Golang with PostgreSQL"})
	})

	AuthRoutes.AuthRoute(router)
	UserRoutes.UserRoute(router)

	server.NoRoute(func(ctx *gin.Context) {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": fmt.Sprintf("Route %s not found", ctx.Request.URL)})
	})
	log.Fatal(server.Run(":" + config.Port))
}

Complete code snippets of the main.go file.

cmd/server/main.go


package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/golang-postgresql-api/config"
	"github.com/wpcodevo/golang-postgresql-api/controllers"
	dbConn "github.com/wpcodevo/golang-postgresql-api/db/sqlc"
	"github.com/wpcodevo/golang-postgresql-api/routes"

	_ "github.com/lib/pq"
)

var (
	server *gin.Engine
	db     *dbConn.Queries
	ctx    context.Context

	AuthController controllers.AuthController
	UserController controllers.UserController
	AuthRoutes     routes.AuthRoutes
	UserRoutes     routes.UserRoutes
)

func init() {
	ctx = context.TODO()
	config, err := config.LoadConfig(".")

	if err != nil {
		log.Fatalf("could not load config: %v", err)
	}

	conn, err := sql.Open(config.PostgreDriver, config.PostgresSource)
	if err != nil {
		log.Fatalf("could not connect to postgres database: %v", err)
	}

	db = dbConn.New(conn)

	fmt.Println("PostgreSQL connected successfully...")

	AuthController = *controllers.NewAuthController(db, ctx)
	UserController = controllers.NewUserController(db, ctx)
	AuthRoutes = routes.NewAuthRoutes(AuthController, db)
	UserRoutes = routes.NewUserRoutes(UserController, db)

	server = gin.Default()
}

func main() {
	config, err := config.LoadConfig(".")

	if err != nil {
		log.Fatalf("could not load config: %v", err)
	}

	corsConfig := cors.DefaultConfig()
	corsConfig.AllowOrigins = []string{config.Origin}
	corsConfig.AllowCredentials = true

	server.Use(cors.New(corsConfig))

	router := server.Group("/api")

	router.GET("/healthchecker", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": "Welcome to Golang with PostgreSQL"})
	})

	AuthRoutes.AuthRoute(router)
	UserRoutes.UserRoute(router)

	server.NoRoute(func(ctx *gin.Context) {
		ctx.JSON(http.StatusNotFound, gin.H{"status": "fail", "message": fmt.Sprintf("Route %s not found", ctx.Request.URL)})
	})
	log.Fatal(server.Run(":" + config.Port))
}


Conclusion

With this Golang, PostgreSQL, and Gin Gonic JWT authentication example with SQLC and Docker-compose, you’ve learned how to implement access and refresh tokens with Golang, PostgreSQL, SQLC, and Gin Gonic.

Golang Gin & PostgreSQL JWT Auth Source Code

You can find the complete source code on GitHub