Implementing JWT Authentication with Redis and Go
Tags: programming, go, backend, redis
Published: 2024-07-05 10:27:36


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m39zknjir2zrh7rrzyqc.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fprdb6if81flva8s23n.png)

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5menjx3mgyuzvjvyjrq8.png)

### 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