Compare commits

..

No commits in common. "master" and "v1.0" have entirely different histories.

12 changed files with 255 additions and 851 deletions

View file

@ -1,4 +1,4 @@
Copyright (C) 2025 Marius Schellenberger Copyright (C) 2016 Marius Schellenberger
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -1,61 +1 @@
# jwt - a simple JSON Web Token library written in Go # jwt - a simple JSON Web Token library 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)
}
}
```

View file

@ -1,63 +0,0 @@
// Copyright (C) 2025 Marius Schellenberger
package jwt
import "sync"
// Blacklist is the blacklisting storage interface
type Blacklist interface {
Add(string, int64) error
Remove(string) error
Check(string) bool
Map() (BlacklistMap, error)
}
// BlacklistMap is the blacklist map structure
type BlacklistMap map[string]int64
// MemBlacklist implements the Blacklist interface
type MemBlacklist struct {
// protects list
sync.RWMutex
list BlacklistMap
}
// NewMemBlacklist implements the Blacklist interface using an in-memory map
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 {
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 {
mb.Lock()
delete(mb.list, sig)
mb.Unlock()
return nil
}
// Check returns true if a token signature is blacklisted and false otherwise
func (mb *MemBlacklist) Check(sig string) (ok bool) {
mb.RLock()
_, ok = mb.list[sig]
mb.RUnlock()
return
}
// Map returns the blacklist in the form of a iterable map structure for cleanup
func (mb *MemBlacklist) Map() (list BlacklistMap, err error) {
list = make(BlacklistMap)
mb.RLock()
for k, v := range mb.list {
list[k] = v
}
mb.RUnlock()
return
}

View file

@ -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
}

View file

@ -1,7 +0,0 @@
// Copyright (C) 2025 Marius Schellenberger
package jwt
import "encoding/base64"
var enc = base64.RawURLEncoding

1
go.mod
View file

@ -1 +0,0 @@
module git.giftfish.de/ston1th/jwt/v3

79
hash.go
View file

@ -1,79 +0,0 @@
// Copyright (C) 2025 Marius Schellenberger
package jwt
import (
"crypto/sha256"
"crypto/sha512"
"hash"
)
const (
HS256Name = "HS256"
HS384Name = "HS384"
HS512Name = "HS512"
)
// NewHash returns the Hash type equal to the input string
func NewHash(alg string) Hash {
switch alg {
case HS256Name:
return hs256
case HS384Name:
return hs384
case HS512Name:
return hs512
}
return nil
}
var (
hs256 = HS256{}
hs384 = HS384{}
hs512 = HS512{}
)
// Hash is the hashsum interface for signing the jwt
type Hash interface {
Hash() hash.Hash
Alg() string
}
// HS256 implements the Hash interface with SHA256
type HS256 struct{}
// Alg returns the algorithm name "HS256"
func (HS256) Alg() string {
return HS256Name
}
// Hash returns a new hash.Hash instance of the SHA256 algorithm
func (HS256) Hash() hash.Hash {
return sha256.New()
}
// HS384 implements the Hash interface with SHA384
type HS384 struct{}
// Alg returns the algorithm name "HS384"
func (HS384) Alg() string {
return HS384Name
}
// Hash returns a new hash.Hash instance of the SHA384 algorithm
func (HS384) Hash() hash.Hash {
return sha512.New384()
}
// HS512 implements the Hash interface with SHA512
type HS512 struct{}
// Alg returns the algorithm name "HS512"
func (HS512) Alg() string {
return HS512Name
}
// Hash returns a new hash.Hash instance of the SHA512 algorithm
func (HS512) Hash() hash.Hash {
return sha512.New()
}

422
jwt.go
View file

@ -1,141 +1,118 @@
// Copyright (C) 2025 Marius Schellenberger
// Package jwt provides a easy to use JSON Web Token and blacklisting library // Package jwt provides a easy to use JSON Web Token and blacklisting library
package jwt package jwt
import ( import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "hash"
"strings"
"sync" "sync"
"time" "time"
) )
const ( const (
// HTTPHeader is the default HTTP Authorization header name // Header is the default HTTP Authorization header name
HTTPHeader = "Authorization" Header = "Authorization"
// DefaultExpiry is the default token expiration time // DefaultExpiry is the default token expiration time
DefaultExpiry = time.Hour * 12 DefaultExpiry = time.Hour * 12
// KeySize is the secret key size typ = "JWT"
KeySize = 64 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 = "."
) )
// DefaultSecretReader is the default secret key generator // Hash represents the three diffrernt hash types
var DefaultSecretReader = rand.Reader type Hash int
const jwtErr = "jwt: " const (
HS256 Hash = iota //SHA256
HS384 //SHA384
HS512 //SHA512
unsupported
)
// String returns the string representation of Hash
func (h Hash) String() string {
switch h {
case HS256:
return "HS256"
case HS384:
return "HS384"
case HS512:
return "HS512"
}
return ""
}
var ( var (
ErrNoJWT = errors.New(jwtErr + "not a json web token") ErrNoJWT = errors.New("no json web token")
ErrEmptyToken = errors.New(jwtErr + "token is empty") ErrUnsupportedAlg = errors.New("unsupported algoritm")
ErrUnsupportedAlg = errors.New(jwtErr + "unsupported algorithm") ErrInvalid = errors.New("token validation failed")
ErrInvalid = errors.New(jwtErr + "token validation failed") ErrBlacklisted = errors.New("token blacklisted")
ErrBlacklisted = errors.New(jwtErr + "token blacklisted") ErrBlacklistNotEnabled = errors.New("blacklisting is not enabled")
ErrBlacklistNotEnabled = errors.New(jwtErr + "blacklisting is not enabled") ErrNBF = errors.New("token not valid yet")
ErrNbf = errors.New(jwtErr + "token not valid yet") ErrEXP = errors.New("token expired")
ErrExp = errors.New(jwtErr + "token expired") ErrMissingEXP = errors.New("missing exp claim")
ErrMissingNbf = errors.New(jwtErr + "missing nbf claim") ErrMissingTokenParts = errors.New("missíng token parts")
ErrMissingExp = errors.New(jwtErr + "missing exp claim") ErrEmptySignature = errors.New("token signature is empty")
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")
) )
type JWTOption func(*JWT) // parseHash returns the hash.Hash type equal to the input string
func parseHash(alg string) (h func() hash.Hash) {
func WithExpiry(expiry time.Duration) JWTOption { switch alg {
return func(jwt *JWT) { case "HS256":
jwt.expiry = expiry h = sha256.New
} case "HS384":
} h = sha512.New384
case "HS512":
func WithBlacklist(blacklist Blacklist) JWTOption { h = sha512.New
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
} }
return
} }
// JWT represents the JSON Web Token signing and blacklisting infrastructure // JWT represents the JSON Web Token signing and blacklisting infrastructure
type JWT struct { type JWT struct {
secretReader io.Reader
key []byte key []byte
expiry time.Duration expiry time.Duration
blacklist Blacklist
nonce bool // protects list
stopOnce sync.Once sync.RWMutex
done chan struct{} blacklist bool
list map[string]int64
stop chan struct{}
} }
// New returns a new JWT object with the given expiry timeout. // 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. // 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. // If blacklisting is enabled, the JWT object leaks a goroutine to garbage-collect expired blacklisted tokens.
// Call the Stop() method to exit the goroutine. // Call the Stop() method to exit the goroutine.
func New(options ...JWTOption) (*JWT, error) { func New(expiry time.Duration, blacklist bool) (*JWT, error) {
jwt := &JWT{} if expiry <= 0 {
for _, option := range options { expiry = DefaultExpiry
option(jwt)
} }
if jwt.expiry <= 0 { key := make([]byte, keySize)
jwt.expiry = DefaultExpiry _, err := rand.Read(key)
}
if jwt.secretReader == nil {
jwt.secretReader = DefaultSecretReader
}
secret := io.LimitReader(jwt.secretReader, KeySize)
key := make([]byte, KeySize)
i, err := secret.Read(key)
if err != nil { if err != nil {
return nil, errors.New(jwtErr + "secret reader error: " + err.Error()) return nil, err
} }
if i < KeySize { jwt := &JWT{
return nil, ErrInvalidKeySize key: key,
expiry: expiry,
blacklist: blacklist,
} }
jwt.key = key if blacklist {
if jwt.blacklist != nil { jwt.list = make(map[string]int64)
jwt.done = make(chan struct{}) jwt.stop = make(chan struct{})
go jwt.clean() go jwt.clean()
} }
return jwt, nil return jwt, nil
} }
// Expiry returns the configured expiry func (jwt *JWT) sum(token string, h func() hash.Hash) []byte {
func (jwt *JWT) Expiry() time.Duration { mac := hmac.New(h, jwt.key)
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)) mac.Write([]byte(token))
return mac.Sum(nil) return mac.Sum(nil)
} }
@ -143,33 +120,33 @@ func (jwt *JWT) sum(token string, h Hash) []byte {
// Invalidate checks if a token is already blacklisted // Invalidate checks if a token is already blacklisted
// If the token is not blacklisted, it will get blacklisted // If the token is not blacklisted, it will get blacklisted
func (jwt *JWT) Invalidate(t *Token) error { func (jwt *JWT) Invalidate(t *Token) error {
if jwt.blacklist == nil { if !jwt.blacklist {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
if t == nil { var (
return ErrTokenIsNil expOK bool
exp int64
)
switch n := t.Claims["exp"].(type) {
case json.Number:
if i, err := n.Int64(); err == nil {
exp = i
expOK = true
} }
if err := jwt.blacklisted(t.Sig()); err != nil { case float64:
exp = int64(n)
expOK = true
}
if !expOK {
return ErrMissingEXP
}
if err := jwt.blacklisted(t.Signature); err != nil {
return err return err
} }
if t.Header.GetString(TypClaim) != Typ { jwt.Lock()
return ErrNoJWT defer jwt.Unlock()
} jwt.list[t.Signature] = exp
h := NewHash(t.Header.GetString(AlgClaim)) return nil
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)
} }
// blacklisted checks if a token is blacklisted // blacklisted checks if a token is blacklisted
@ -177,120 +154,193 @@ func (jwt *JWT) blacklisted(sig string) error {
if sig == "" { if sig == "" {
return ErrEmptySignature return ErrEmptySignature
} }
if jwt.blacklist.Check(sig) { jwt.RLock()
defer jwt.RUnlock()
_, ok := jwt.list[sig]
if ok {
return ErrBlacklisted return ErrBlacklisted
} }
return nil return nil
} }
// clean looks for expired blacklisted tokens and removes them // clean will look for expired blacklisted tokens and removes them
func (jwt *JWT) clean() { func (jwt *JWT) clean() {
for { for {
t := time.NewTimer(time.Hour) t := time.NewTimer(time.Hour)
select { select {
case <-t.C: case <-t.C:
case <-jwt.done: case <-jwt.stop:
t.Stop() t.Stop()
return return
} }
now := Now() now := time.Now().UTC().Unix()
m, err := jwt.blacklist.Map() jwt.Lock()
if err != nil { defer jwt.Unlock()
continue for k, v := range jwt.list {
}
for k, v := range m {
if now > v { if now > v {
jwt.blacklist.Remove(k) delete(jwt.list, k)
} }
} }
} }
} }
// Sign will sign the provided token using the secret key // 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: // This will overwrite existing 'exp' and 'nbf' claims
// 'exp': Now + expiry specified at New() or DefaultExpiry (UTC)
// 'nbf': Now (UTC)
func (jwt *JWT) Sign(t *Token) (err error) { func (jwt *JWT) Sign(t *Token) (err error) {
if t == nil { now := time.Now().UTC()
return ErrTokenIsNil t.Claims["exp"] = now.Add(jwt.expiry).Unix()
t.Claims["nbf"] = now.Unix()
h, err := json.Marshal(t.Header)
if err != nil {
return
} }
if t.Header.GetString(TypClaim) != Typ { c, err := json.Marshal(t.Claims)
return ErrNoJWT if err != nil {
return
} }
h := NewHash(t.Header.GetString(AlgClaim)) var hf func() hash.Hash
if h == nil { switch a := t.Header["alg"].(type) {
case string:
hf = parseHash(a)
if hf == nil {
return ErrUnsupportedAlg return ErrUnsupportedAlg
} }
now := time.Now() default:
if _, ok := t.Claims.Get(ExpClaim); !ok { return ErrUnsupportedAlg
t.Claims.Set(ExpClaim, newExp(now, jwt.expiry))
} }
if _, ok := t.Claims.Get(NbfClaim); !ok { t.Data = strings.Join([]string{base64.URLEncoding.EncodeToString(h), base64.URLEncoding.EncodeToString(c)}, ".")
t.Claims.Set(NbfClaim, NewNbf(now)) t.Raw = strings.Join([]string{t.Data, base64.URLEncoding.EncodeToString(jwt.sum(t.Data, hf))}, ".")
}
if jwt.nonce {
t.Claims.Set(NonceClaim, enc.EncodeToString(key16()))
}
head, err := json.Marshal(t.Header)
if err != nil {
return
}
claims, err := json.Marshal(t.Claims)
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
return return
} }
// Verify will verify the provided token using the secret key // Verify will verify the provided token using the secret key
func (jwt *JWT) Verify(t *Token) error { func (jwt *JWT) Verify(t *Token) error {
if t == nil { now := time.Now().UTC().Unix()
return ErrTokenIsNil var (
exp int64
nbf int64
expOK bool
nbfOK bool
hf func() hash.Hash
)
switch t := t.Header["typ"].(type) {
case string:
if t != typ {
return ErrNoJWT
} }
if jwt.blacklist != nil { default:
if err := jwt.blacklisted(t.Sig()); err != nil { return ErrNoJWT
}
switch a := t.Header["alg"].(type) {
case string:
hf = parseHash(a)
if hf == 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 expOK && jwt.blacklist {
err := jwt.blacklisted(t.Signature)
if err != nil {
return err return err
} }
} }
now := Now() if expOK && now > exp {
if t.Header.GetString(TypClaim) != Typ { return ErrEXP
return ErrNoJWT
} }
h := NewHash(t.Header.GetString(AlgClaim)) if nbfOK && now < nbf {
if h == nil { return ErrNBF
return ErrUnsupportedAlg
} }
exp := t.Claims.GetInt64(ExpClaim) if !hmac.Equal(jwt.sum(t.Data, hf), t.RawSignature) {
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 !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) {
return ErrInvalid return ErrInvalid
} }
return nil 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 { func (jwt *JWT) Stop() error {
if jwt.blacklist == nil { if !jwt.blacklist {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
jwt.stopOnce.Do(func() { select {
close(jwt.done) case jwt.stop <- struct{}{}:
}) close(jwt.stop)
default:
}
return nil return nil
} }
// DecodeToken decodes a raw string token into a *Token object
func DecodeToken(token string) (t *Token, err error) {
parts := strings.Split(token, ".")
if len(parts) < 3 {
return nil, ErrMissingTokenParts
}
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
Exp int64
Header map[string]interface{}
Claims map[string]interface{}
}
// NewToken returns a new *Token using the provided hash algorithm and claims
func NewToken(hash Hash, claims map[string]interface{}) *Token {
return &Token{
Header: map[string]interface{}{
"alg": hash.String(),
"typ": typ,
},
Claims: claims,
}
}

View file

@ -1,250 +1,25 @@
// Copyright (C) 2025 Marius Schellenberger
package jwt package jwt
import ( import (
"bytes"
"errors"
"testing" "testing"
"time" "time"
) )
var claims = Claims{ func TestValidate(t *testing.T) {
jwt, _ := New(0, true)
token := NewToken(HS256, map[string]interface{}{
"sub": "1234567890", "sub": "1234567890",
"name": "John Doe", "name": "John Doe",
"admin": true, "admin": true,
"fizz": "buzz", "fizz": "buzz",
} })
err := jwt.Sign(token)
func TestValidate(t *testing.T) { t.Log(err, token)
jwt, err := New( nt, err := DecodeToken(token.Raw)
WithExpiry(time.Second), t.Log(nt, err)
WithBlacklist(NewMemBlacklist()), time.Sleep(time.Second)
) t.Log(jwt.Verify(nt))
if err != nil { jwt.Invalidate(nt)
t.Error(err) t.Log(jwt.Verify(nt))
} jwt.Stop()
token := NewToken(claims.Copy(), nil)
err = jwt.Sign(token)
if err != nil {
t.Error(err)
}
_, err = DecodeToken("")
if err == nil {
t.Error(errors.New("token is empty"))
}
nt, err := DecodeToken(token.String())
if err != nil {
t.Error(err)
}
err = jwt.Verify(nt)
if err != nil {
t.Error(err)
}
time.Sleep(time.Second * 2)
err = jwt.Verify(nt)
if err == nil {
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)
}
err = jwt.Invalidate(nt)
if err == nil {
t.Error(errors.New("double invalidate"))
}
err = jwt.Verify(nt)
if err == nil {
t.Error(errors.New("token should be blacklisted"))
}
err = jwt.Stop()
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))
if err != nil {
t.Error(err)
}
token := NewToken(claims.Copy(), nil)
err = jwt.Sign(token)
if err != nil {
t.Error(err)
}
_, err = DecodeToken("")
if err == nil {
t.Error(errors.New("token is empty"))
}
nt, err := DecodeToken(token.String())
if err != nil {
t.Error(err)
}
err = jwt.Verify(nt)
if err != nil {
t.Error(err)
}
time.Sleep(time.Second * 2)
err = jwt.Verify(nt)
if err == nil {
t.Error(errors.New("token should be expired"))
}
err = jwt.Invalidate(nt)
if err == nil {
t.Error(errors.New("blacklisting should be disabled"))
}
}
func TestEmptySecretReader(t *testing.T) {
_, err := New(
WithExpiry(time.Second),
WithSecret(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")),
)
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)
}
} }

11
rand.go
View file

@ -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
}

24
time.go
View file

@ -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()
}

View file

@ -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
}