Using sessions in your Go API


Let’s try to implement sessions based auth in Golang with Redis to store sessions data.

How it should work?

  • User send email and password on login endpoint
  • Backend trying to authenticate user
  • Backend creates two entries in Redis - entry like:
inegrht8eh54874he486he4786:{user_id: 1, email: "kazanlug@gmail.com"}

And array struct with user’s email as a key and all active sessions inside: for example:

kazanlug@gmail.com:[inegrht8eh54874he486he4786, 34j687ej86de7h568hr875687rdh8, j65j468rj7d65897r89789d]

You can store session id at cookie for example or at local storage at client like JWT.

Instead of JWT (which contains payload data), session just an identifier for data which can be stored at any storage (Redis in our example).

Let’s check it.

Redis session store

We using github.com/go-redis/redis package here to connect to Redis instance.

Nothing special, simple struct with interface and basic implementation of methods.

You can read about them at https://redis.io/commands.

package store

import (
	"log"
	"github.com/go-redis/redis"
	"github.com/pkg/errors"
	"os"
	"authy/logger"
	"time"
)

type UserSession struct {
	CreatedAt time.Time `json:"created_at"`
	UserID    uint `json:"user_id"`
	Email  string `json:"email"`
}

type SerializableStore interface {
	Get(string) (UserSession, error)
	Set(string, string) error
	HMSet(string, map[string]interface{}) error
	HGetAll(string) (map[string]string, error)
	HDel(string, string) int64
	HMLen(string) int64
	Delete(string) error
}

type redisStore struct {
	client *redis.Client
}

func CustomRedisStore() SerializableStore {
	client := redis.NewClient(&redis.Options{
		Addr:     os.Getenv("redis_url"),
		Password: os.Getenv("redis_password"),
		DB:       0,
	})

	_, err := client.Ping().Result()
	if err != nil {
		log.Fatalf("Failed to ping Redis: %v", err)
	}

	return &redisStore {
		client: client,
	}
}

func (r redisStore) Delete(id string) error {

	_, err := r.client.Del(id).Result()
	if err != nil {
		return errors.Wrap(err, "problem")
	}
	return nil
}

func (r redisStore) Get(id string) (UserSession, error) {
	bs, err := r.client.Get(id).Bytes()
	session := string(bs)

	if err != nil {
		logger.Error(err)
	}

	str := FromGOB64(session)

	return str, nil
}

func (r redisStore) Set(id string, session string) error {

	if err := r.client.Set(id, session, 0).Err(); err != nil {
		return errors.Wrap(err, "failed to save session to redis")
	}

	return nil
}

func (r redisStore) HMSet(key string, data map[string]interface{}) error {

	if err := r.client.HMSet(key, data).Err(); err != nil {
		return errors.Wrap(err, "failed to save session array to redis")
	}

	return nil
}

func (r redisStore) HGetAll(key string) (map[string]string, error) {
	data, err := r.client.HGetAll(key).Result()
	return data, err
}


func (r redisStore) HMLen(key string) int64 {
	data := r.client.HLen(key)
	return data.Val()
}

func (r redisStore) HDel(key, val string) int64 {
	data := r.client.HDel(key, val)
	return data.Val()
}

Business logic

Okay, now let’s implement our logic, I’m skipping some methods implementation, because in fact, it’s just a logic for calling Redis related methods and checking for existing session.

Interesting part here - about generating new session id from uuid, but as you can see it’s super simple.

package sessions

import (
	uuid "github.com/satori/go.uuid"
	"net/http"
	"authy/logger"
	"authy/store"
	u "authy/tools"
	"time"
)

func GenerateSessionID() string {
	uuid := uuid.NewV4()
	sessionID := uuid.String()
	return sessionID
}

func BuildUserSession(Authenticated, id uint, email string) store.UserSession{
	return store.UserSession{
		CreatedAt: time.Now(),
		UserID:    id,
		Email: email,
		Authenticated: Authenticated,
	}
}

func UpdateSession(sessionID string, session store.UserSession) store.UserSession {
	// method imlementation
}

func SaveOrAddRedisSession(Authenticated, TwoFactorEnabled bool, IPAddr, email string, id uint, r *http.Request, w http.ResponseWriter) string {
  // method imlementation
}

func DropRedisSession(sessionID string, r *http.Request, w http.ResponseWriter) map[string]interface{} {
	// method imlementation
}

func GetUserSession(sessionID string) store.UserSession {
	// method imlementation
}

func GetUserSessions(key string) map[string]string {
	// method imlementation
}

func AddToUserSessionsArray(email string, data map[string]interface{}) {
	// method imlementation
}

func RemoveFromUserSessionsArray(key, val string) {
	// method imlementation
}

That’s all - just save data in your store, add your session to user’s session list (don’t forget to remove them after logout btw) and save as separate key as well.

Super easy and simple.