git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/statelesstoken/stateless.go (about) 1 package statelesstoken 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "errors" 7 "strings" 8 "time" 9 10 "git.sr.ht/~pingoo/stdx/crypto" 11 "git.sr.ht/~pingoo/stdx/uuid" 12 ) 13 14 const maxStatelessData = 128 15 16 const ( 17 SecretSize = crypto.KeySize256 18 HashSize = crypto.KeySize256 19 ) 20 21 var ( 22 ErrTokenIsNotValid = errors.New("token is not valid") 23 ErrDataIsTooLong = errors.New("data is too long") 24 ) 25 26 type Stateless struct { 27 version uint8 28 payload statelessPayload 29 signature []byte 30 str string 31 } 32 33 type statelessPayload struct { 34 ID uuid.UUID `json:"id"` 35 Exp time.Time `json:"exp"` 36 Data string `json:"data"` 37 } 38 39 func (token *Stateless) String() string { 40 return token.str 41 } 42 43 func (token *Stateless) Version() uint8 { 44 return token.version 45 } 46 47 func (token *Stateless) ID() uuid.UUID { 48 return token.payload.ID 49 } 50 51 func (token *Stateless) Data() string { 52 return token.payload.Data 53 } 54 55 func (token *Stateless) Verify(key []byte) (err error) { 56 parts := strings.Split(token.str, ".") 57 if len(parts) != 3 { 58 err = ErrTokenIsNotValid 59 return 60 } 61 62 versionAndPayload := parts[0] + "." + parts[1] 63 64 signature, err := crypto.Mac(key, []byte(versionAndPayload), crypto.KeySize256) 65 if err != nil { 66 return 67 } 68 69 if !crypto.ConstantTimeCompare(signature, token.signature) { 70 err = ErrTokenIsNotValid 71 return 72 } 73 74 return 75 } 76 77 func New(key []byte, id uuid.UUID, expire time.Time, data string) (token Stateless, err error) { 78 if len(data) > maxStatelessData { 79 err = ErrDataIsTooLong 80 return 81 } 82 83 token.version = 1 84 token.payload = statelessPayload{ 85 ID: id, 86 Exp: expire.UTC(), 87 Data: data, 88 } 89 90 payloadJson, err := json.Marshal(token.payload) 91 if err != nil { 92 return 93 } 94 95 payloadBase64 := base64.RawURLEncoding.EncodeToString(payloadJson) 96 97 token.str = "v1" + "." + payloadBase64 98 99 token.signature, err = crypto.Mac(key, []byte(token.str), crypto.KeySize256) 100 if err != nil { 101 return 102 } 103 104 token.str += "." + base64.RawURLEncoding.EncodeToString(token.signature) 105 106 return 107 } 108 109 func ParseStateless(tokenStr string) (token Stateless, err error) { 110 if len(tokenStr) > 170+maxStatelessData { 111 err = ErrDataIsTooLong 112 return 113 } 114 115 token.str = tokenStr 116 parts := strings.Split(tokenStr, ".") 117 if len(parts) != 3 { 118 err = ErrTokenIsNotValid 119 return 120 } 121 122 // Version 123 switch parts[0] { 124 case "v1": 125 token.version = 1 126 default: 127 err = ErrTokenIsNotValid 128 return 129 } 130 131 // Payload 132 payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) 133 if err != nil { 134 err = ErrTokenIsNotValid 135 return 136 } 137 138 err = json.Unmarshal(payloadJSON, &token.payload) 139 if err != nil { 140 err = ErrTokenIsNotValid 141 return 142 } 143 144 // Signature 145 token.signature, err = base64.RawURLEncoding.DecodeString(parts[2]) 146 if err != nil { 147 err = ErrTokenIsNotValid 148 return 149 } 150 151 return 152 }