github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/store.go (about)

     1  package auth
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/cozy/cozy-stack/pkg/config/config"
    11  	"github.com/cozy/cozy-stack/pkg/crypto"
    12  	"github.com/cozy/cozy-stack/pkg/prefixer"
    13  	"github.com/redis/go-redis/v9"
    14  )
    15  
    16  // Store is essentially an object to store and retrieve confirmation codes
    17  type Store interface {
    18  	AddCode(db prefixer.Prefixer) (string, error)
    19  	GetCode(db prefixer.Prefixer, code string) (bool, error)
    20  }
    21  
    22  // storeTTL is the time an entry stay alive
    23  var storeTTL = 5 * time.Minute
    24  
    25  // storeCleanInterval is the time interval between each cleanup.
    26  var storeCleanInterval = 1 * time.Hour
    27  
    28  var mu sync.Mutex
    29  var globalStore Store
    30  
    31  // GetStore returns the store for temporary move objects.
    32  func GetStore() Store {
    33  	mu.Lock()
    34  	defer mu.Unlock()
    35  	if globalStore != nil {
    36  		return globalStore
    37  	}
    38  	cli := config.GetConfig().SessionStorage
    39  	if cli == nil {
    40  		globalStore = newMemStore()
    41  	} else {
    42  		ctx := context.Background()
    43  		globalStore = &redisStore{cli, ctx}
    44  	}
    45  	return globalStore
    46  }
    47  
    48  type memStore struct {
    49  	mu   sync.Mutex
    50  	vals map[string]time.Time
    51  }
    52  
    53  func newMemStore() Store {
    54  	store := &memStore{vals: make(map[string]time.Time)}
    55  	go store.cleaner()
    56  	return store
    57  }
    58  
    59  func (s *memStore) cleaner() {
    60  	for range time.Tick(storeCleanInterval) {
    61  		now := time.Now()
    62  		for k, v := range s.vals {
    63  			if now.After(v) {
    64  				delete(s.vals, k)
    65  			}
    66  		}
    67  	}
    68  }
    69  
    70  func (s *memStore) AddCode(db prefixer.Prefixer) (string, error) {
    71  	code := makeSecret()
    72  	s.mu.Lock()
    73  	defer s.mu.Unlock()
    74  	key := confirmKey(db, code)
    75  	s.vals[key] = time.Now().Add(storeTTL)
    76  	return code, nil
    77  }
    78  
    79  func (s *memStore) GetCode(db prefixer.Prefixer, code string) (bool, error) {
    80  	s.mu.Lock()
    81  	defer s.mu.Unlock()
    82  	key := confirmKey(db, code)
    83  	exp, ok := s.vals[key]
    84  	if !ok {
    85  		return false, nil
    86  	}
    87  	if time.Now().After(exp) {
    88  		delete(s.vals, key)
    89  		return false, nil
    90  	}
    91  	return true, nil
    92  }
    93  
    94  type redisStore struct {
    95  	c   redis.UniversalClient
    96  	ctx context.Context
    97  }
    98  
    99  func (s *redisStore) AddCode(db prefixer.Prefixer) (string, error) {
   100  	code := makeSecret()
   101  	key := confirmKey(db, code)
   102  	if err := s.c.Set(s.ctx, key, "1", storeTTL).Err(); err != nil {
   103  		return "", err
   104  	}
   105  	return code, nil
   106  }
   107  
   108  func (s *redisStore) GetCode(db prefixer.Prefixer, code string) (bool, error) {
   109  	key := confirmKey(db, code)
   110  	n, err := s.c.Exists(s.ctx, key).Result()
   111  	if errors.Is(err, redis.Nil) || n == 0 {
   112  		return false, nil
   113  	}
   114  	if err != nil {
   115  		return false, err
   116  	}
   117  	return true, nil
   118  }
   119  
   120  func confirmKey(db prefixer.Prefixer, suffix string) string {
   121  	return db.DBPrefix() + ":confirm_auth:" + suffix
   122  }
   123  
   124  func makeSecret() string {
   125  	return hex.EncodeToString(crypto.GenerateRandomBytes(8))
   126  }