NovAi-Go Programming 101

The place to learn, get update and enhance you golang skill

Follow publication

Building a JWT Authentication System with Refresh Tokens in Go

Nova Novriansyah
NovAi-Go Programming 101
4 min readNov 6, 2024

--

In this article, we’ll build a secure JWT-based authentication system in Go using the Gin framework. We’ll cover creating both access and refresh tokens, setting up protected routes, and creating a refresh token endpoint for renewing access tokens.

Overview of JWT with Refresh Tokens

JWT (JSON Web Token) is a popular way to handle authentication securely. Typically, an access token has a short lifespan, while a refresh token lasts longer. Users authenticate with an access token, and when it expires, they can use the refresh token to get a new access token without needing to log in again.

Key Components:

1. Access Token: Short-lived, used for authenticating requests.

2. Refresh Token: Long-lived, used to obtain new access tokens without re-authenticating the user.

Project Setup

1. Install Gin and JWT dependencies:

go get -u github.com/gin-gonic/gin
go get -u github.com/dgrijalva/jwt-go

2. Create a new Go file (e.g., main.go) for our application.

Implementing JWT Authentication with Refresh Tokens

Here’s the full code with comments explaining each section.

package main

import (
"net/http"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)

// Secret keys for signing tokens
const secretKey = "my_secret_key"
const refreshSecretKey = "my_refresh_secret_key"

func main() {
r := gin.Default()

// Route for generating tokens
r.POST("/login", handleLogin)

// Route for refreshing tokens
r.POST("/refresh", handleRefresh)

// Middleware to check JWT on every request
r.Use(authMiddleware())

// Protected routes
r.GET("/api/general", handleGeneralResource)
r.GET("/api/admin", adminMiddleware(), handleAdminResource)

r.Run(":8080")
}

Step 1: Creating the Login Endpoint

The /login endpoint authenticates the user, generates both an access token and a refresh token, and sends them in the response.

func handleLogin(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")

// Example user authentication logic
var role string
if username == "admin" && password == "password" {
role = "admin"
} else if username == "user" && password == "password" {
role = "user"
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}

// Create the access token (expires in 15 minutes)
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": username,
"role": role,
"exp": time.Now().Add(time.Minute * 15).Unix(),
})

accessTokenString, err := accessToken.SignedString([]byte(secretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate access token"})
return
}

// Create the refresh token (expires in 7 days)
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": username,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
})

refreshTokenString, err := refreshToken.SignedString([]byte(refreshSecretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate refresh token"})
return
}

c.JSON(http.StatusOK, gin.H{
"access_token": accessTokenString,
"refresh_token": refreshTokenString,
})
}

Step 2: Setting Up the Refresh Token Endpoint

The /refresh endpoint verifies the refresh token and, if valid, issues a new access token.

func handleRefresh(c *gin.Context) {
refreshTokenString := c.PostForm("refresh_token")

// Parse and verify the refresh token
refreshToken, err := jwt.Parse(refreshTokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, http.ErrAbortHandler
}
return []byte(refreshSecretKey), nil
})

if err != nil || !refreshToken.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid refresh token"})
return
}

// Generate a new access token if refresh token is valid
if claims, ok := refreshToken.Claims.(jwt.MapClaims); ok && refreshToken.Valid {
username := claims["username"].(string)

newAccessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": username,
"exp": time.Now().Add(time.Minute * 15).Unix(),
})

newAccessTokenString, err := newAccessToken.SignedString([]byte(secretKey))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate new access token"})
return
}

c.JSON(http.StatusOK, gin.H{
"access_token": newAccessTokenString,
})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid refresh token"})
}
}

Step 3: Creating Middleware to Protect Routes

The authMiddleware checks if the provided access token is valid. If not, it denies access.

func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")

// Parse the access token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, http.ErrAbortHandler
}
return []byte(secretKey), nil
})

// Check if token is valid
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}

// Set claims in the context
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
c.Set("claims", claims)
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}

c.Next()
}
}

Step 4: Setting Up Role-Based Access

The adminMiddleware restricts certain routes to users with the “admin” role.

func adminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
claims := c.MustGet("claims").(jwt.MapClaims)
role := claims["role"].(string)

if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
c.Abort()
return
}

c.Next()
}
}

Step 5: Adding Protected Endpoints

We create two endpoints: one open to any authenticated user (/api/general) and another restricted to admin users (/api/admin).

func handleGeneralResource(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "General resource accessed successfully",
})
}

func handleAdminResource(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Admin resource accessed successfully",
})
}

Testing the System

1. Login: Send a POST request to /login with valid credentials (username and password). You’ll receive an access_token and a refresh_token.

2. Access Protected Resource: Send a GET request to /api/general or /api/admin, including the Authorization header with the access_token.

3. Token Expiry and Refresh: After the access_token expires, send a POST request to /refresh with the refresh_token to get a new access_token.

In this article, we’ve built a secure JWT-based authentication system with Gin in Go. Our system uses:

Access Tokens for short-lived access to resources.

Refresh Tokens to extend sessions without re-authentication.

This setup is efficient and secure, giving users a seamless experience while maintaining robust security through short-lived tokens and refresh options.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

NovAi-Go Programming 101
NovAi-Go Programming 101

Published in NovAi-Go Programming 101

The place to learn, get update and enhance you golang skill

Nova Novriansyah
Nova Novriansyah

Written by Nova Novriansyah

C|CISO, CEH, CC, CVA,CertBlockchainPractitioner, Google Machine Learning , Tensorflow, Unity Cert, Arduino Cert, AWS Arch Cert. CTO, IT leaders. Platform owners

Responses (1)

Write a response