Building a JWT Authentication System with Refresh Tokens in Go
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.