Overview
When building authentication systems, it's often necessary to extend token validity while users remain active. This implementation provides a sliding window expiration mechanism where tokens are automatically refreshed as long as the user continues to make requests.
Implementation
package api
import (
"net/http"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/gin-gonic/gin"
)
type AuthController struct {}
var signingSecret = []byte("application_secret_key")
type UserClaims struct {
Username string `json:"username"`
jwt.StandardClaims
}
func (ac AuthController) Login(ctx *gin.Context) {
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := ctx.ShouldBindJSON(&credentials); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
return
}
if credentials.Username != "admin" || credentials.Password != "secret123" {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
token, err := CreateToken(credentials.Username)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
return
}
ctx.JSON(http.StatusOK, gin.H{"token": token})
}
func CreateToken(username string) (string, error) {
expirationTime := time.Now().Add(1 * time.Minute)
claims := &UserClaims{
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(signingSecret)
if err != nil {
return "", err
}
return tokenString, nil
}
func (ac AuthController) AuthMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenString := ctx.GetHeader("Authorization")
if tokenString == "" {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization header"})
ctx.Abort()
return
}
// Attempt to refresh token if needed
newToken, refreshErr := CheckAndRefreshToken(tokenString)
if refreshErr != nil {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Token validation failed"})
ctx.Abort()
return
}
// Parse and validate the original token
claims, parseErr := ParseToken(tokenString)
if parseErr != nil {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Token parsing failed"})
ctx.Abort()
return
}
// Set refreshed token in response header
ctx.Header("Authorization", newToken)
ctx.Set("user_claims", claims)
ctx.Next()
}
}
func (ac AuthController) Dashboard(ctx *gin.Context) {
claims, _ := ctx.Get("user_claims")
userClaims := claims.(*UserClaims)
ctx.JSON(http.StatusOK, gin.H{"message": "Welcome back, " + userClaims.Username})
}
func ParseToken(tokenString string) (*UserClaims, error) {
claims := &UserClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return signingSecret, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return nil, err
}
return nil, err
}
if !token.Valid {
return nil, err
}
return claims, nil
}
func CheckAndRefreshToken(tokenString string) (string, error) {
claims := &UserClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return signingSecret, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return "", err
}
return "", err
}
if !token.Valid {
return "", err
}
// Check if token is approaching expiration
if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) < 1*time.Minute {
// Generate a new token with extended expiration
newToken, err := CreateToken(claims.Username)
if err != nil {
return "", err
}
return newToken, nil
}
return tokenString, nil
}
Client-Side Integration
The cleint application must handle token refresh on each request:
- Authenticate with login credentials to obtain the initial token
- Include the token in the Authorization header for all API calls
- Extract the new token from the response header after each request
- Replace the stored token with the refreshed token for subsequent requests
This approach ensures that as long as the user remains active, their session extends automatically. The token lifetime resets to the full duration on each request, effectively creating a sliding expiration window that keeps users logged in during active sessions.
Key Components
| Component | Purpose |
|---|---|
CreateToken |
Generates a new JWT with configurable expiration |
ParseToken |
Validates and extracts claims from token |
CheckAndRefreshToken |
Validates token and issues new one if near expiration |
AuthMiddleware |
Intercepts requests to validate and refresh tokens |
The refresh logic checks if less then one minute remains before expiration. If so, a fresh token with the full one-minute window is returned. Otherwise, the original token passes through unchanged.