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  }