Implementing JWT Authentication with Redis and Go
Redis is an in memory data structure store often used as a cache and message broker but can as well be used as a primary database.
Redis is well suited for JWT authentication tokens due to Speed, Scalability, TTL(Time To Live), Session Storage.
I will use own repository to showcase how have I used it and if you want to follow video format you can check out bellow YouTube videos.
https://youtu.be/SQrsDZU_D5k
https://youtu.be/NissLXyZ2Zw
Introduction
In JWT authentication like mine it makes sense if you have a primary database like PostgreSQL, Mongo or Firebase(like in my example).
Be sure to run command
go get github.com/redis/go-redis/v9
Ones installed figure out if you want to run Redis in a Docker Image or PaaS provider like Upstash👇
https://console.upstash.com/login
You can use Docker to run Redis.
docker run — name recepie -p 6379:6379 -d redis:latest
a nutshell my frontend is with React but backend is Go with Gin Web Framework main database Firebase and Redis will save userID with AuthToken ones logged in from frontend.
Architecture & Code
Architecures change based on implementation This is how I have approached it to make it organized.
Application Configuration
LoadConfigurations: Set up initial configurations, including connecting to Firebase and Redis.
func (a *Application) LoadConfigurations() error { ctx := context.Background() fireClient, err := GetFirestoreClient(ctx) if err != nil { return err } a.FireClient = fireClient fireAuth, err := GetAuthClient(ctx) if err != nil { return err } a.FireAuth = fireAuth // Redis env variable depending if PaaS server provided if not 6379 port used. // So basically Docker image. a.RedisPort = envy.Get("REDIS_SERVER", "localhost:6379") redisClient, err := RedisConnect(a.RedisPort) if err != nil { return err } a.RedisClient = redisClient a.ListenPort = envy.Get("PORT", "8080") return nil }
RedisConnect Function: Connect to Redis, handling both Docker and PaaS setups.
func redisClientPort(port string, envExists bool) (*redis.Client, error) { if envExists { opt, err := redis.ParseURL(port) if err != nil { return nil, fmt.Errorf("failed to parse Redis URL: %w", err) } return redis.NewClient(opt), nil } return redis.NewClient(&redis.Options{ Addr: port, Password: "", DB: 0, }), nil } func RedisConnect(port string) (*redis.Client, error) { _, ok := os.LookupEnv(port) client, err := redisClientPort(port, ok) if err != nil { return nil, fmt.Errorf("failed to ping Redis server: %w", err) } ping, err := client.Ping(context.Background()).Result() if err != nil { return nil, fmt.Errorf("failed to ping Redis server: %w", err) } fmt.Println("Ping response from Redis:", ping) return client, nil }
Start Function: Initialize the Gin router and set up routes and middleware.
func Start(a *app.Application) error { router := gin.New() router.Use(cors.New(md.CORSMiddleware())) api.SetCache(router, a.RedisClient) api.SetRoutes(router, a.FireClient, a.FireAuth, a.RedisClient) err := router.Run(":" + a.ListenPort) if err != nil { return err } return nil }
SetCache Function: Define endpoints for setting cache and other requests handled by Firebase.
// api/controller.go func SetCache(router *gin.Engine, client *redis.Client) { router.POST("/set-cache", func(c *gin.Context) { setUserCache(c, client) }) router.GET("/check-expiration", func(c *gin.Context) { checkTokenExpiration(c, client) }) } func SetRoutes(router *gin.Engine, client *firestore.Client, auth *auth.Client, redisClient *redis.Client) { router.OPTIONS("/*any", func(c *gin.Context) { c.Status(http.StatusOK) }) // In Gin Use means that it's required router.Use(func(c *gin.Context) { authToken := getUserCache(c, redisClient) md.AuthJWT(auth, authToken)(c) }) router.GET("/", func(c *gin.Context) { showRecepies(c, client) }) router.POST("/", func(c *gin.Context) { addRecepie(c, client) })
In SetRouters we do main requests to alter db data. AuthJWT is set by client side on Firebase to authenticate any request made to database.
AuthToken is what we will be talking about and it is passed in to AuthJWT to authenticate or deny user of any interaction.
Next up is to set up main GET, SET actions including TTL for session management.
// models/cache.go type UserCache struct { UserID string `redis:"UserID"` AuthToken string `redis:"AuthToken"` }
The above struct is the only important one to parse incoming data from React. ☝️
Handling Cache Operations
Get User Cache: Retrieve user cache from Redis.
// api/cache.go func getUserCache(ctx *gin.Context, client *redis.Client) string { userID := ctx.Query("userID") authToken, err := models.GetUserCacheToken(ctx, client, userID) if err != nil { log.Printf("Issues retriving Cached Token %v", err) return "" } return authToken } // models/cache.go func GetUserCacheToken(ctx *gin.Context, client *redis.Client, userID string) (string, error) { key := fmt.Sprintf("user:%s", userID) cache, err := client.HGetAll(ctx, key).Result() if err != nil { return "", fmt.Errorf("failed to get cache: %v", err) } authToken, ok := cache["AuthToken"] if !ok { return "", fmt.Errorf("AuthToken not found in cache") } return authToken, nil }
Set User Cache: Set user cache in Redis with TTL.
// api/cache.go func setUserCache(ctx *gin.Context, client *redis.Client) { var userCache models.UserCache err := models.UnmarshallRequestBodyToAPIData(ctx.Request.Body, &userCache) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{ "error": "Unable to parse data", }) return } key := fmt.Sprintf("user:%s", userCache.UserID) _, notExists := client.HGetAll(ctx, key).Result() if notExists == nil { userCache.SetCachedToken(ctx, client, key) return } } // models/cache.go func (c *UserCache) SetCachedToken(ctx *gin.Context, client *redis.Client, key string) { fields := map[string]interface{}{ "UserID": c.UserID, "AuthToken": c.AuthToken, } err := client.HSet(ctx, key, fields).Err() if err != nil { log.Printf("Issues setting Cached Token %v", err) } client.Expire(ctx, key, 7*24*time.Hour) }
If you are interested in React section let me know otherwise Github repo will be listed bellow.
As on React login through Firebase it creates a user with authToken and passes to Go backend if exists ignore otherwise create.
Check Token Expiration
Check Token Expiration: Check if the token has expired.
// api/cache.go func checkTokenExpiration(ctx *gin.Context, client *redis.Client) { userID := ctx.Query("userID") key := fmt.Sprintf("user:%s", userID) ttl, err := client.TTL(ctx, key).Result() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get TTL", }) return } expired := ttl <= 0 ctx.JSON(http.StatusOK, expired) }
Conclusion
This setup provides a robust structure for managing JWT authentication with Redis in a Go application, ensuring efficient session management and token validation. If there are any questions feel free to ask(or ask GPT) my repo you can find bellow.
https://github.com/Mozes721/RecipesApp