github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/token/service.go (about)

     1  package token
     2  
     3  import (
     4  	"errors"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/cozy/cozy-stack/pkg/cache"
     9  	"github.com/cozy/cozy-stack/pkg/crypto"
    10  	"github.com/cozy/cozy-stack/pkg/prefixer"
    11  )
    12  
    13  const TokenLen = 32
    14  
    15  var (
    16  	ErrInvalidToken     = errors.New("invalid token")
    17  	ErrInvalidNamespace = errors.New("invalid namespace")
    18  )
    19  
    20  type Operation string
    21  
    22  var (
    23  	EmailUpdate Operation = "email_update"
    24  	MagicLink   Operation = "magic_link"
    25  )
    26  
    27  // TokenService is a [Service] implementation based on [cache.Cache].
    28  //
    29  // Note: Depending on the cache implementation setup the storage can
    30  // be store in-memory a reboot would invalidate all the existing tokens.
    31  //
    32  // This can be the case for the self-hosted stacks using [cache.InMemory].
    33  type TokenService struct {
    34  	cache cache.Cache
    35  }
    36  
    37  // NewService instantiates a new [CacheService].
    38  func NewService(cache cache.Cache) *TokenService {
    39  	return &TokenService{cache}
    40  }
    41  
    42  // GenerateAndSave generate a random token and save it into the storage for the specified duration.
    43  //
    44  // Once the lifetime is expired, the token is deleted and will be never valid.
    45  func (s *TokenService) GenerateAndSave(db prefixer.Prefixer, op Operation, resource string, lifetime time.Duration) (string, error) {
    46  	token := crypto.GenerateRandomString(TokenLen)
    47  
    48  	key, err := generateKey(db, op, token)
    49  	if err != nil {
    50  		return "", err
    51  	}
    52  
    53  	// TODO: The cache should returns an error and should be matched here.
    54  	s.cache.SetNX(key, []byte(resource), lifetime)
    55  
    56  	return token, nil
    57  }
    58  
    59  // Validate will validate that the user as a matching token.
    60  //
    61  // If the token doesn't match any content or an expired content the error [ErrInvalidToken] is returned.
    62  func (s *TokenService) Validate(db prefixer.Prefixer, op Operation, resource, token string) error {
    63  	key, err := generateKey(db, op, token)
    64  	if err != nil {
    65  		return err
    66  	}
    67  
    68  	// TODO: The cache should return an error and we should parse it.
    69  	val, ok := s.cache.Get(key)
    70  	if !ok {
    71  		return ErrInvalidToken
    72  	}
    73  
    74  	if string(val) != resource {
    75  		return ErrInvalidToken
    76  	}
    77  
    78  	// TODO: Should we delete automatically the token valid once validated?
    79  
    80  	return nil
    81  }
    82  
    83  func generateKey(db prefixer.Prefixer, ns Operation, code string) (string, error) {
    84  	var nsStr string
    85  
    86  	switch ns {
    87  	case EmailUpdate, MagicLink:
    88  		nsStr = string(ns)
    89  	default:
    90  		return "", ErrInvalidNamespace
    91  	}
    92  
    93  	return strings.Join([]string{db.DBPrefix(), nsStr, code}, ":"), nil
    94  }