Implementing JWT Authentication with Redis and Go

7V9R...YaGh
5 Jul 2024
37


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

Get fast shipping, movies & more with Amazon Prime

Start free trial

Enjoy this blog? Subscribe to Mozes711

0 Comments