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:
- API with Golang, PostgreSQL, SQLC & Gin Gonic: Project Setup
- Golang, SQLC, and PostgreSQL: JWT Access & Refresh Tokens
- Golang CRUD RESTful API with SQLC and PostgreSQL
Related articles:
- Golang & MongoDB: JWT Authentication and Authorization
- API with Golang + MongoDB: Send HTML Emails with Gomail
- API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
- Build Golang gRPC Server and Client: SignUp User & Verify Email
- Build Golang gRPC Server and Client: Access & Refresh Tokens
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
-Sign in to your account with your credentials
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.
-Retrieve the authenticated user’s credentials
-Request a new access token when it expires
-Logout the authenticated 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.
RESOURCE | HTTP METHOD | ROUTE | DESCRIPTION |
---|---|---|---|
users | GET | /api/users/me | Get the signed-in user’s credentials |
auth | POST | /api/auth/register | Register a new user |
auth | POST | /api/auth/login | Sign in the user |
auth | GET | /api/auth/refresh | Request a new access token |
auth | GET | /api/auth/logout | Logout 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:
- Connect to PostgreSQL database
- 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
I use similar of this implementation, but i didn’t understand something in this example. We wrote RefreshAccessToken() method but we never used it. We should have make some logic like; if our access token’s exp time is in past, we should controll refresh_token. And if it is alive, we should refresh all the tokens. I actually came here to see how you are doing this.
The access token can be refreshed in two ways:
1. On the backend
With this approach, you will create a separate function that will be evoked to return new access and refresh tokens. Let’s call this function
generateToken()
During the authentication process on the backend, you will check the access token to see if it has expired and call the
generateToken()
function to generate new access and refresh tokens.After the tokens have been generated and you’ve finished processing the original request, you will assign the tokens to the cookie object.
This implementation is what you wanted which is also not a bad idea. With this approach, there is no need to create the
RefreshAccessToken()
route handler.2. On the frontend app
When a user logs into the API, three cookies will be sent to the client – access, refresh, and logged_in cookies. Both the access and refresh tokens are HTTPOnly cookies and the frontend app can not access them. The logged_in cookie, on the other hand, is not HTTPOnly and it also has the same expiration time as the access token.
Let’s assume you are using Axios to make the requests. You will add a request interceptor to the Axios instance where you’ll try to access the logged_in cookie. Since the logged_in cookie has the same expiration time as the access token, when the logged_in cookie is not available then it means the access token has expired.
Once you know the access token has expired, you will fire a request to the
/api/auth/refresh
endpoint to get a new access token before re-trying the original request. TheRefreshAccessToken()
handler is required if you will be refreshing the access token with the frontend app.