diff --git a/LICENSE b/LICENSE index 74cee2b..115e96e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2025 Marius Schellenberger +Copyright (C) 2018 Marius Schellenberger All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 74aa8a3..ad89f76 100644 --- a/README.md +++ b/README.md @@ -1,61 +1 @@ -# jwt - a simple JSON Web Token library written in Go - -## Usage - -``` -package main - -import ( - "fmt" - "git.giftfish.de/ston1th/jwt" - "log" - "time" -) - -func main() { - // create a new signing instance - JWT, err := jwt.New(time.Hour, jwt.NewMemBlacklist(), nil) - if err != nil { - log.Fatal(err) - } - - // create a new token - t := jwt.NewToken(jwt.Claims{ - "username": "admin", - }, nil) - - // sign the token - err = JWT.Sign(t) - if err != nil { - log.Fatal(err) - } - - // print the token string - token := t.String() - fmt.Println(token) - - // decode the token string - newToken, err := jwt.DecodeToken(token) - if err != nil { - log.Fatal(err) - } - - // verify the decoded token - err = JWT.Verify(newToken) - if err != nil { - log.Fatal(err) - } - - // add the token to the blacklist - err = JWT.Invalidate(newToken) - if err != nil { - log.Fatal(err) - } - - // try to verify the blacklisted token, it should fail - err = JWT.Verify(newToken) - if err != nil { - log.Fatal(err) - } -} -``` +# jwt - a simple JSON Web Token library in Go diff --git a/blacklist.go b/blacklist.go index 180c5fd..024796d 100644 --- a/blacklist.go +++ b/blacklist.go @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Marius Schellenberger +// Copyright (C) 2018 Marius Schellenberger package jwt @@ -6,10 +6,10 @@ import "sync" // Blacklist is the blacklisting storage interface type Blacklist interface { - Add(string, int64) error - Remove(string) error + Add(string, int64) + Remove(string) Check(string) bool - Map() (BlacklistMap, error) + Map() BlacklistMap } // BlacklistMap is the blacklist map structure @@ -22,25 +22,23 @@ type MemBlacklist struct { list BlacklistMap } -// NewMemBlacklist implements the Blacklist interface using an in-memory map +// NewMemBlacklist func NewMemBlacklist() *MemBlacklist { return &MemBlacklist{list: make(BlacklistMap)} } // Add adds a new token signature with expiration time to the blacklist -func (mb *MemBlacklist) Add(sig string, exp int64) error { +func (mb *MemBlacklist) Add(sig string, exp int64) { mb.Lock() mb.list[sig] = exp mb.Unlock() - return nil } // Remove deletes a token signature from the blacklist -func (mb *MemBlacklist) Remove(sig string) error { +func (mb *MemBlacklist) Remove(sig string) { mb.Lock() delete(mb.list, sig) mb.Unlock() - return nil } // Check returns true if a token signature is blacklisted and false otherwise @@ -52,7 +50,7 @@ func (mb *MemBlacklist) Check(sig string) (ok bool) { } // Map returns the blacklist in the form of a iterable map structure for cleanup -func (mb *MemBlacklist) Map() (list BlacklistMap, err error) { +func (mb *MemBlacklist) Map() (list BlacklistMap) { list = make(BlacklistMap) mb.RLock() for k, v := range mb.list { diff --git a/claims.go b/claims.go deleted file mode 100644 index a5f4364..0000000 --- a/claims.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2025 Marius Schellenberger - -package jwt - -// Claims is the claim type of the token -// The Claims map is not goroutine safe -type Claims map[string]interface{} - -// Get returns a value from the claims map -func (c Claims) Get(key string) (v interface{}, ok bool) { - v, ok = c[key] - return -} - -// GetString returns a string from the claims map -func (c Claims) GetString(key string) (s string) { - if v, ok := c.Get(key); ok && v != nil { - s, _ = v.(string) - } - return -} - -// GetBool returns a bool from the claims map -func (c Claims) GetBool(key string) (b bool) { - if v, ok := c.Get(key); ok && v != nil { - b, _ = v.(bool) - } - return -} - -// GetInt returns an int from the claims map -func (c Claims) GetInt(key string) (i int) { - i = int(c.GetInt64(key)) - return -} - -// GetInt64 returns an int64 from the claims map -func (c Claims) GetInt64(key string) (i int64) { - if v, ok := c.Get(key); ok && v != nil { - switch val := v.(type) { - case int64: - i = val - case float64: - i = int64(val) - } - } - return -} - -// getFloat64 returns a float64 and ok from the claims map -func (c Claims) GetFloat64(key string) (f float64) { - if v, ok := c.Get(key); ok && v != nil { - f, _ = v.(float64) - } - return -} - -// Set sets the value of key in the claims map, if not nil -func (c Claims) Set(key string, v interface{}) { - if c == nil { - return - } - c[key] = v -} - -// Delete removes the key from the claims map -func (c Claims) Delete(key string) { - delete(c, key) -} - -// Copy returns a new Claims map -func (c Claims) Copy() (n Claims) { - n = make(Claims) - for k, v := range c { - n[k] = v - } - return -} diff --git a/encoding.go b/encoding.go deleted file mode 100644 index 3693f5f..0000000 --- a/encoding.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (C) 2025 Marius Schellenberger - -package jwt - -import "encoding/base64" - -var enc = base64.RawURLEncoding diff --git a/go.mod b/go.mod deleted file mode 100644 index 6752b42..0000000 --- a/go.mod +++ /dev/null @@ -1 +0,0 @@ -module git.giftfish.de/ston1th/jwt/v3 diff --git a/hash.go b/hash.go index e47c0cc..2c224ab 100644 --- a/hash.go +++ b/hash.go @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Marius Schellenberger +// Copyright (C) 2018 Marius Schellenberger package jwt @@ -14,25 +14,19 @@ const ( HS512Name = "HS512" ) -// NewHash returns the Hash type equal to the input string -func NewHash(alg string) Hash { +// ParseHash returns the Hash type equal to the input string +func ParseHash(alg string) Hash { switch alg { case HS256Name: - return hs256 + return NewHS256() case HS384Name: - return hs384 + return NewHS384() case HS512Name: - return hs512 + return NewHS512() } return nil } -var ( - hs256 = HS256{} - hs384 = HS384{} - hs512 = HS512{} -) - // Hash is the hashsum interface for signing the jwt type Hash interface { Hash() hash.Hash @@ -42,6 +36,11 @@ type Hash interface { // HS256 implements the Hash interface with SHA256 type HS256 struct{} +// NewHS256 returns a new HS256 instance +func NewHS256() Hash { + return HS256{} +} + // Alg returns the algorithm name "HS256" func (HS256) Alg() string { return HS256Name @@ -55,6 +54,11 @@ func (HS256) Hash() hash.Hash { // HS384 implements the Hash interface with SHA384 type HS384 struct{} +// NewHS384 returns a new HS384 instance +func NewHS384() Hash { + return HS384{} +} + // Alg returns the algorithm name "HS384" func (HS384) Alg() string { return HS384Name @@ -68,6 +72,11 @@ func (HS384) Hash() hash.Hash { // HS512 implements the Hash interface with SHA512 type HS512 struct{} +// NewHS512 returns a new HS512 instance +func NewHS512() Hash { + return HS512{} +} + // Alg returns the algorithm name "HS512" func (HS512) Alg() string { return HS512Name diff --git a/jwt.go b/jwt.go index f9ce6af..2106f13 100644 --- a/jwt.go +++ b/jwt.go @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Marius Schellenberger +// Copyright (C) 2018 Marius Schellenberger // Package jwt provides a easy to use JSON Web Token and blacklisting library package jwt @@ -6,134 +6,85 @@ package jwt import ( "crypto/hmac" "crypto/rand" + "encoding/base64" "encoding/json" "errors" "io" - "sync" + "strings" "time" ) const ( - // HTTPHeader is the default HTTP Authorization header name - HTTPHeader = "Authorization" + // Header is the default HTTP Authorization header name + Header = "Authorization" // DefaultExpiry is the default token expiration time DefaultExpiry = time.Hour * 12 // KeySize is the secret key size KeySize = 64 - // Typ is the JWT type - Typ = "JWT" - // TypClaim is the typ claim name - TypClaim = "typ" - // AlgClaim is the alg claim name - AlgClaim = "alg" - // ExpClaim is the exp claim name - ExpClaim = "exp" - // NbfClaim is the nbf claim name - NbfClaim = "nbf" - // NonceClaim - NonceClaim = "nonce" - // TokenSeparator is the tokens separator char - TokenSeparator = "." + typ = "JWT" ) // DefaultSecretReader is the default secret key generator var DefaultSecretReader = rand.Reader -const jwtErr = "jwt: " - var ( - ErrNoJWT = errors.New(jwtErr + "not a json web token") - ErrEmptyToken = errors.New(jwtErr + "token is empty") - ErrUnsupportedAlg = errors.New(jwtErr + "unsupported algorithm") - ErrInvalid = errors.New(jwtErr + "token validation failed") - ErrBlacklisted = errors.New(jwtErr + "token blacklisted") - ErrBlacklistNotEnabled = errors.New(jwtErr + "blacklisting is not enabled") - ErrNbf = errors.New(jwtErr + "token not valid yet") - ErrExp = errors.New(jwtErr + "token expired") - ErrMissingNbf = errors.New(jwtErr + "missing nbf claim") - ErrMissingExp = errors.New(jwtErr + "missing exp claim") - ErrMissingTokenParts = errors.New(jwtErr + "missing token parts") - ErrEmptySignature = errors.New(jwtErr + "token signature is empty") - ErrTokenIsNil = errors.New(jwtErr + "token is nil") - ErrInvalidKeySize = errors.New(jwtErr + "invalid secret key size") + ErrNoJWT = errors.New("not a json web token") + ErrEmptyToken = errors.New("token is empty") + ErrUnsupportedAlg = errors.New("unsupported algorithm") + ErrInvalid = errors.New("token validation failed") + ErrBlacklisted = errors.New("token blacklisted") + ErrBlacklistNotEnabled = errors.New("blacklisting is not enabled") + ErrNBF = errors.New("token not valid yet") + ErrEXP = errors.New("token expired") + ErrMissingEXP = errors.New("missing exp claim") + ErrMissingTokenParts = errors.New("missing token parts") + ErrEmptySignature = errors.New("token signature is empty") + ErrTokenIsNil = errors.New("token is nil") + ErrInvalidKeySize = errors.New("invalid secret key size") ) -type JWTOption func(*JWT) - -func WithExpiry(expiry time.Duration) JWTOption { - return func(jwt *JWT) { - jwt.expiry = expiry - } -} - -func WithBlacklist(blacklist Blacklist) JWTOption { - return func(jwt *JWT) { - jwt.blacklist = blacklist - } -} -func WithSecret(secret io.Reader) JWTOption { - return func(jwt *JWT) { - jwt.secretReader = secret - } -} -func WithNonce() JWTOption { - return func(jwt *JWT) { - jwt.nonce = true - } -} - // JWT represents the JSON Web Token signing and blacklisting infrastructure type JWT struct { - secretReader io.Reader - key []byte - expiry time.Duration - blacklist Blacklist - nonce bool - stopOnce sync.Once - done chan struct{} + key []byte + expiry time.Duration + + blacklist Blacklist + done chan struct{} } // New returns a new JWT object with the given expiry timeout. // If the timeout is less or equal to zero the default expiry (12 hours) is used. -// The secret key size needs to be at least 64 bytes. -// If secret is nil the DefaultSecretReader is used. // If blacklisting is enabled, the JWT object leaks a goroutine to garbage-collect expired blacklisted tokens. // Call the Stop() method to exit the goroutine. -func New(options ...JWTOption) (*JWT, error) { - jwt := &JWT{} - for _, option := range options { - option(jwt) +func New(expiry time.Duration, blacklist Blacklist, secret io.Reader) (*JWT, error) { + if expiry <= 0 { + expiry = DefaultExpiry } - if jwt.expiry <= 0 { - jwt.expiry = DefaultExpiry + if secret == nil { + secret = DefaultSecretReader } - if jwt.secretReader == nil { - jwt.secretReader = DefaultSecretReader - } - secret := io.LimitReader(jwt.secretReader, KeySize) + secret = io.LimitReader(secret, KeySize) key := make([]byte, KeySize) i, err := secret.Read(key) if err != nil { - return nil, errors.New(jwtErr + "secret reader error: " + err.Error()) + return nil, errors.New("secret reader error: " + err.Error()) } if i < KeySize { return nil, ErrInvalidKeySize } - jwt.key = key - if jwt.blacklist != nil { + jwt := &JWT{ + key: key, + expiry: expiry, + blacklist: blacklist, + } + if blacklist != nil { jwt.done = make(chan struct{}) go jwt.clean() } return jwt, nil } -// Expiry returns the configured expiry -func (jwt *JWT) Expiry() time.Duration { - return jwt.expiry -} - -// sum calculates the HMAC hash sum of the token func (jwt *JWT) sum(token string, h Hash) []byte { mac := hmac.New(h.Hash, jwt.key) mac.Write([]byte(token)) @@ -149,27 +100,28 @@ func (jwt *JWT) Invalidate(t *Token) error { if t == nil { return ErrTokenIsNil } + var ( + expOK bool + exp int64 + ) + switch n := t.Claims["exp"].(type) { + case json.Number: + if i, err := n.Int64(); err == nil { + exp = i + expOK = true + } + case float64: + exp = int64(n) + expOK = true + } + if !expOK { + return ErrMissingEXP + } if err := jwt.blacklisted(t.Sig()); err != nil { return err } - if t.Header.GetString(TypClaim) != Typ { - return ErrNoJWT - } - h := NewHash(t.Header.GetString(AlgClaim)) - if h == nil { - return ErrUnsupportedAlg - } - exp := t.Claims.GetInt64(ExpClaim) - switch { - case exp == 0: - return ErrMissingExp - case Now() > exp: - return ErrExp - } - if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) { - return ErrInvalid - } - return jwt.blacklist.Add(t.Sig(), exp) + jwt.blacklist.Add(t.Sig(), exp) + return nil } // blacklisted checks if a token is blacklisted @@ -193,12 +145,8 @@ func (jwt *JWT) clean() { t.Stop() return } - now := Now() - m, err := jwt.blacklist.Map() - if err != nil { - continue - } - for k, v := range m { + now := time.Now().UTC().Unix() + for k, v := range jwt.blacklist.Map() { if now > v { jwt.blacklist.Remove(k) } @@ -207,31 +155,34 @@ func (jwt *JWT) clean() { } // Sign will sign the provided token using the secret key -// If the 'exp' and 'nbf' claims do not exist, they will be written to the default values in UNIX format: -// 'exp': Now + expiry specified at New() or DefaultExpiry (UTC) -// 'nbf': Now (UTC) +// This will overwrite existing 'exp' and 'nbf' claims func (jwt *JWT) Sign(t *Token) (err error) { if t == nil { return ErrTokenIsNil } - if t.Header.GetString(TypClaim) != Typ { + header := t.Header() + switch t := header["typ"].(type) { + case string: + if t != typ { + return ErrNoJWT + } + default: return ErrNoJWT } - h := NewHash(t.Header.GetString(AlgClaim)) - if h == nil { + var h Hash + switch a := header["alg"].(type) { + case string: + h = ParseHash(a) + if h == nil { + return ErrUnsupportedAlg + } + default: return ErrUnsupportedAlg } - now := time.Now() - if _, ok := t.Claims.Get(ExpClaim); !ok { - t.Claims.Set(ExpClaim, newExp(now, jwt.expiry)) - } - if _, ok := t.Claims.Get(NbfClaim); !ok { - t.Claims.Set(NbfClaim, NewNbf(now)) - } - if jwt.nonce { - t.Claims.Set(NonceClaim, enc.EncodeToString(key16())) - } - head, err := json.Marshal(t.Header) + now := time.Now().UTC() + t.Claims["exp"] = now.Add(jwt.expiry).Unix() + t.Claims["nbf"] = now.Unix() + head, err := json.Marshal(header) if err != nil { return } @@ -239,10 +190,8 @@ func (jwt *JWT) Sign(t *Token) (err error) { if err != nil { return } - t.data = enc.EncodeToString(head) + TokenSeparator + enc.EncodeToString(claims) - t.rawSignature = jwt.sum(t.data, h) - t.signature = enc.EncodeToString(t.rawSignature) - t.raw = t.data + TokenSeparator + t.signature + t.data = strings.Join([]string{base64.URLEncoding.EncodeToString(head), base64.URLEncoding.EncodeToString(claims)}, ".") + t.raw = strings.Join([]string{t.data, base64.URLEncoding.EncodeToString(jwt.sum(t.data, h))}, ".") return } @@ -251,32 +200,64 @@ func (jwt *JWT) Verify(t *Token) error { if t == nil { return ErrTokenIsNil } + now := time.Now().UTC().Unix() + var ( + exp int64 + nbf int64 + expOK bool + nbfOK bool + h Hash + ) + header := t.Header() + switch t := header["typ"].(type) { + case string: + if t != typ { + return ErrNoJWT + } + default: + return ErrNoJWT + } + switch a := header["alg"].(type) { + case string: + h = ParseHash(a) + if h == nil { + return ErrUnsupportedAlg + } + default: + return ErrUnsupportedAlg + } + + switch n := t.Claims["exp"].(type) { + case json.Number: + if i, err := n.Int64(); err == nil { + exp = i + expOK = true + } + case float64: + exp = int64(n) + expOK = true + } + switch n := t.Claims["nbf"].(type) { + case json.Number: + if i, err := n.Int64(); err == nil { + nbf = i + nbfOK = true + } + case float64: + nbf = int64(n) + nbfOK = true + } if jwt.blacklist != nil { - if err := jwt.blacklisted(t.Sig()); err != nil { + err := jwt.blacklisted(t.Sig()) + if err != nil { return err } } - now := Now() - if t.Header.GetString(TypClaim) != Typ { - return ErrNoJWT + if expOK && now > exp { + return ErrEXP } - h := NewHash(t.Header.GetString(AlgClaim)) - if h == nil { - return ErrUnsupportedAlg - } - exp := t.Claims.GetInt64(ExpClaim) - switch { - case exp == 0: - return ErrMissingExp - case now > exp: - return ErrExp - } - nbf := t.Claims.GetInt64(NbfClaim) - switch { - case nbf == 0: - return ErrMissingNbf - case now < nbf: - return ErrNbf + if nbfOK && now < nbf { + return ErrNBF } if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) { return ErrInvalid @@ -284,13 +265,103 @@ func (jwt *JWT) Verify(t *Token) error { return nil } -// Stop will end the cleaner goroutine execution +// Stop will end the cleaner goroutinge execution +// Calling Stop twice or more will panic func (jwt *JWT) Stop() error { if jwt.blacklist == nil { return ErrBlacklistNotEnabled } - jwt.stopOnce.Do(func() { - close(jwt.done) - }) + close(jwt.done) return nil } + +// DecodeToken decodes a raw string token into a *Token object +func DecodeToken(token string) (t *Token, err error) { + if token == "" { + return nil, ErrEmptyToken + } + parts := strings.Split(token, ".") + if len(parts) < 3 { + return nil, ErrMissingTokenParts + } + if len(parts) > 3 { + return nil, ErrNoJWT + } + for _, v := range parts { + if v == "" { + return nil, ErrNoJWT + } + } + t = new(Token) + header, err := base64.URLEncoding.DecodeString(parts[0]) + if err != nil { + return + } + err = json.Unmarshal(header, &t.header) + if err != nil { + return + } + claims, err := base64.URLEncoding.DecodeString(parts[1]) + if err != nil { + return + } + err = json.Unmarshal(claims, &t.Claims) + if err != nil { + return + } + t.rawSignature, err = base64.URLEncoding.DecodeString(parts[2]) + t.signature = parts[2] + t.data = strings.Join(parts[:2], ".") + t.raw = token + return +} + +// Token is the JWT token representation +type Token struct { + raw string + data string + signature string + rawSignature []byte + header map[string]interface{} + Claims map[string]interface{} +} + +// String returns the tokens encoded string +func (t *Token) String() string { + return t.raw +} + +// Sig returns the Tokens URLEncoded signature +func (t *Token) Sig() string { + return t.signature +} + +// RawSig returns the Tokens raw signature +func (t *Token) RawSig() []byte { + return t.rawSignature +} + +// Header returns the Tokens header +func (t *Token) Header() map[string]interface{} { + return t.header +} + +// Data returns the first two token fields +func (t *Token) Data() string { + return t.data +} + +// NewToken returns a new *Token using the provided hash algorithm and claims +// If h is nil HS256 will be used +func NewToken(claims map[string]interface{}, h Hash) *Token { + if h == nil { + h = NewHS256() + } + return &Token{ + header: map[string]interface{}{ + "alg": h.Alg(), + "typ": typ, + }, + Claims: claims, + } +} diff --git a/jwt_test.go b/jwt_test.go index 664cf8a..30cfbfe 100644 --- a/jwt_test.go +++ b/jwt_test.go @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Marius Schellenberger +// Copyright (C) 2018 Marius Schellenberger package jwt @@ -9,22 +9,17 @@ import ( "time" ) -var claims = Claims{ - "sub": "1234567890", - "name": "John Doe", - "admin": true, - "fizz": "buzz", -} - func TestValidate(t *testing.T) { - jwt, err := New( - WithExpiry(time.Second), - WithBlacklist(NewMemBlacklist()), - ) + jwt, err := New(time.Second, NewMemBlacklist(), nil) if err != nil { t.Error(err) } - token := NewToken(claims.Copy(), nil) + token := NewToken(map[string]interface{}{ + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "fizz": "buzz", + }, nil) err = jwt.Sign(token) if err != nil { t.Error(err) @@ -47,21 +42,6 @@ func TestValidate(t *testing.T) { t.Error(errors.New("token should be expired")) } - err = jwt.Invalidate(nt) - if err == nil { - t.Error(errors.New("token is expired")) - } - - nt.Claims.Delete(ExpClaim) - err = jwt.Sign(nt) - if err != nil { - t.Error(err) - } - err = jwt.Verify(nt) - if err != nil { - t.Error(err) - } - err = jwt.Invalidate(nt) if err != nil { t.Error(err) @@ -70,7 +50,6 @@ func TestValidate(t *testing.T) { if err == nil { t.Error(errors.New("double invalidate")) } - err = jwt.Verify(nt) if err == nil { t.Error(errors.New("token should be blacklisted")) @@ -79,46 +58,19 @@ func TestValidate(t *testing.T) { if err != nil { t.Error(err) } - // should not panic - err = jwt.Stop() - if err != nil { - t.Error(err) - } -} - -func TestNonce(t *testing.T) { - jwt, err := New( - WithExpiry(time.Second), - WithNonce(), - ) - if err != nil { - t.Error(err) - } - token := NewToken(claims.Copy(), nil) - err = jwt.Sign(token) - if err != nil { - t.Error(err) - } - nt, err := DecodeToken(token.String()) - if err != nil { - t.Error(err) - } - err = jwt.Verify(nt) - if err != nil { - t.Error(err) - } - nonce := nt.Claims.GetString(NonceClaim) - if nonce == "" { - t.Error(errors.New("nonce is empty")) - } } func TestNoBlacklist(t *testing.T) { - jwt, err := New(WithExpiry(time.Second)) + jwt, err := New(time.Second, nil, nil) if err != nil { t.Error(err) } - token := NewToken(claims.Copy(), nil) + token := NewToken(map[string]interface{}{ + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "fizz": "buzz", + }, nil) err = jwt.Sign(token) if err != nil { t.Error(err) @@ -148,103 +100,15 @@ func TestNoBlacklist(t *testing.T) { } func TestEmptySecretReader(t *testing.T) { - _, err := New( - WithExpiry(time.Second), - WithSecret(new(bytes.Buffer)), - ) + _, err := New(time.Second, nil, new(bytes.Buffer)) if err == nil { t.Error(errors.New("error should be secret reader error")) } } func TestInvalidSecretReader(t *testing.T) { - _, err := New( - WithExpiry(time.Second), - WithSecret(bytes.NewBufferString("123")), - ) + _, err := New(time.Second, nil, bytes.NewBufferString("123")) if err != ErrInvalidKeySize { t.Error(errors.New("error should be invalid key size")) } } - -func BenchmarkDecodeToken(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), nil) - jwt.Sign(t) - token := t.String() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - DecodeToken(token) - } -} - -func BenchmarkNew(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - New() - } -} - -func BenchmarkNewWithBlacklist(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - New(WithBlacklist(NewMemBlacklist())) - } -} - -func BenchmarkSignHS256(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), nil) - b.ReportAllocs() - for i := 0; i < b.N; i++ { - jwt.Sign(t) - } -} - -func BenchmarkSignHS384(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), NewHash(HS384Name)) - b.ReportAllocs() - for i := 0; i < b.N; i++ { - jwt.Sign(t) - } -} - -func BenchmarkSignHS512(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), NewHash(HS512Name)) - b.ReportAllocs() - for i := 0; i < b.N; i++ { - jwt.Sign(t) - } -} - -func BenchmarkVerifyHS256(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), nil) - jwt.Sign(t) - b.ReportAllocs() - for i := 0; i < b.N; i++ { - jwt.Verify(t) - } -} - -func BenchmarkVerifyHS384(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), NewHash(HS384Name)) - jwt.Sign(t) - b.ReportAllocs() - for i := 0; i < b.N; i++ { - jwt.Verify(t) - } -} - -func BenchmarkVerifyHS512(b *testing.B) { - jwt, _ := New() - t := NewToken(claims.Copy(), NewHash(HS512Name)) - jwt.Sign(t) - b.ReportAllocs() - for i := 0; i < b.N; i++ { - jwt.Verify(t) - } -} diff --git a/rand.go b/rand.go deleted file mode 100644 index bd1b191..0000000 --- a/rand.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (C) 2025 Marius Schellenberger - -package jwt - -import "crypto/rand" - -func key16() []byte { - b := make([]byte, 16) - rand.Read(b) - return b -} diff --git a/time.go b/time.go deleted file mode 100644 index 77e151f..0000000 --- a/time.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Marius Schellenberger - -package jwt - -import "time" - -// Now returns the current time in UTC Unix format -func Now() int64 { - return NewNbf(time.Now()) -} - -// NewNbf returns a new 'not before' date -func NewNbf(t time.Time) int64 { - return t.UTC().Unix() -} - -// NewExp returns a new expiration date -func NewExp(d time.Duration) int64 { - return newExp(time.Now(), d) -} - -func newExp(t time.Time, d time.Duration) int64 { - return t.UTC().Add(d).Unix() -} diff --git a/token.go b/token.go deleted file mode 100644 index 772415c..0000000 --- a/token.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (C) 2025 Marius Schellenberger - -package jwt - -import ( - "encoding/json" - "strings" -) - -// Token represents a JWT token -type Token struct { - raw string - data string - signature string - rawSignature []byte - Header Claims - Claims Claims -} - -// String returns the tokens encoded string -func (t *Token) String() string { - return t.raw -} - -// Sig returns the tokens URLEncoded signature -func (t *Token) Sig() string { - return t.signature -} - -// RawSig returns the tokens raw signature -func (t *Token) RawSig() []byte { - return t.rawSignature -} - -// Data returns the first two URLEncoded token fields -func (t *Token) Data() string { - return t.data -} - -// NewToken returns a new *Token using the provided hash algorithm and claims -// If claims is nil, an empty map is used -// If hash is nil, HS256 is used -func NewToken(claims Claims, hash Hash) *Token { - if claims == nil { - claims = make(Claims) - } - if hash == nil { - hash = NewHash(HS256Name) - } - return &Token{ - Header: Claims{ - AlgClaim: hash.Alg(), - TypClaim: Typ, - }, - Claims: claims, - } -} - -// DecodeToken decodes a raw string token into a *Token object -func DecodeToken(token string) (t *Token, err error) { - if token == "" { - return nil, ErrEmptyToken - } - parts := strings.Split(token, TokenSeparator) - if len(parts) < 3 { - return nil, ErrMissingTokenParts - } - if len(parts) > 3 { - return nil, ErrNoJWT - } - for _, v := range parts { - if v == "" { - return nil, ErrNoJWT - } - } - t = new(Token) - header, err := enc.DecodeString(parts[0]) - if err != nil { - return - } - err = json.Unmarshal(header, &t.Header) - if err != nil { - return - } - claims, err := enc.DecodeString(parts[1]) - if err != nil { - return - } - err = json.Unmarshal(claims, &t.Claims) - if err != nil { - return - } - t.rawSignature, err = enc.DecodeString(parts[2]) - t.signature = parts[2] - t.data = parts[0] + TokenSeparator + parts[1] - t.raw = token - return -}