DEV Community

Ricards Taujenis
Ricards Taujenis

Posted on

Implementing JWT Authentication with Redis and Go

Image description

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

In 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.

Image description

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)

}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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)
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (1)

Collapse
 
plutov profile image
Alex Pliutau

Great write-up, we have a bunch of articles on Go in our Newsletter, check it out - packagemain.tech