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 }