Compare commits

..

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

12 changed files with 274 additions and 611 deletions

View file

@ -1,4 +1,4 @@
Copyright (C) 2025 Marius Schellenberger Copyright (C) 2018 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,4 +1,4 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt
@ -6,10 +6,10 @@ import "sync"
// Blacklist is the blacklisting storage interface // Blacklist is the blacklisting storage interface
type Blacklist interface { type Blacklist interface {
Add(string, int64) error Add(string, int64)
Remove(string) error Remove(string)
Check(string) bool Check(string) bool
Map() (BlacklistMap, error) Map() BlacklistMap
} }
// BlacklistMap is the blacklist map structure // BlacklistMap is the blacklist map structure
@ -22,25 +22,23 @@ type MemBlacklist struct {
list BlacklistMap list BlacklistMap
} }
// NewMemBlacklist implements the Blacklist interface using an in-memory map // NewMemBlacklist
func NewMemBlacklist() *MemBlacklist { func NewMemBlacklist() *MemBlacklist {
return &MemBlacklist{list: make(BlacklistMap)} return &MemBlacklist{list: make(BlacklistMap)}
} }
// Add adds a new token signature with expiration time to the blacklist // 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.Lock()
mb.list[sig] = exp mb.list[sig] = exp
mb.Unlock() mb.Unlock()
return nil
} }
// Remove deletes a token signature from the blacklist // Remove deletes a token signature from the blacklist
func (mb *MemBlacklist) Remove(sig string) error { func (mb *MemBlacklist) Remove(sig string) {
mb.Lock() mb.Lock()
delete(mb.list, sig) delete(mb.list, sig)
mb.Unlock() mb.Unlock()
return nil
} }
// Check returns true if a token signature is blacklisted and false otherwise // 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 // 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) list = make(BlacklistMap)
mb.RLock() mb.RLock()
for k, v := range mb.list { for k, v := range mb.list {

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

33
hash.go
View file

@ -1,4 +1,4 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt
@ -14,25 +14,19 @@ const (
HS512Name = "HS512" HS512Name = "HS512"
) )
// NewHash returns the Hash type equal to the input string // ParseHash returns the Hash type equal to the input string
func NewHash(alg string) Hash { func ParseHash(alg string) Hash {
switch alg { switch alg {
case HS256Name: case HS256Name:
return hs256 return NewHS256()
case HS384Name: case HS384Name:
return hs384 return NewHS384()
case HS512Name: case HS512Name:
return hs512 return NewHS512()
} }
return nil return nil
} }
var (
hs256 = HS256{}
hs384 = HS384{}
hs512 = HS512{}
)
// Hash is the hashsum interface for signing the jwt // Hash is the hashsum interface for signing the jwt
type Hash interface { type Hash interface {
Hash() hash.Hash Hash() hash.Hash
@ -42,6 +36,11 @@ type Hash interface {
// HS256 implements the Hash interface with SHA256 // HS256 implements the Hash interface with SHA256
type HS256 struct{} type HS256 struct{}
// NewHS256 returns a new HS256 instance
func NewHS256() Hash {
return HS256{}
}
// Alg returns the algorithm name "HS256" // Alg returns the algorithm name "HS256"
func (HS256) Alg() string { func (HS256) Alg() string {
return HS256Name return HS256Name
@ -55,6 +54,11 @@ func (HS256) Hash() hash.Hash {
// HS384 implements the Hash interface with SHA384 // HS384 implements the Hash interface with SHA384
type HS384 struct{} type HS384 struct{}
// NewHS384 returns a new HS384 instance
func NewHS384() Hash {
return HS384{}
}
// Alg returns the algorithm name "HS384" // Alg returns the algorithm name "HS384"
func (HS384) Alg() string { func (HS384) Alg() string {
return HS384Name return HS384Name
@ -68,6 +72,11 @@ func (HS384) Hash() hash.Hash {
// HS512 implements the Hash interface with SHA512 // HS512 implements the Hash interface with SHA512
type HS512 struct{} type HS512 struct{}
// NewHS512 returns a new HS512 instance
func NewHS512() Hash {
return HS512{}
}
// Alg returns the algorithm name "HS512" // Alg returns the algorithm name "HS512"
func (HS512) Alg() string { func (HS512) Alg() string {
return HS512Name return HS512Name

383
jwt.go
View file

@ -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 provides a easy to use JSON Web Token and blacklisting library
package jwt package jwt
@ -6,134 +6,85 @@ package jwt
import ( import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"sync" "strings"
"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 // KeySize is the secret key size
KeySize = 64 KeySize = 64
// Typ is the JWT type typ = "JWT"
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 // DefaultSecretReader is the default secret key generator
var DefaultSecretReader = rand.Reader var DefaultSecretReader = rand.Reader
const jwtErr = "jwt: "
var ( var (
ErrNoJWT = errors.New(jwtErr + "not a json web token") ErrNoJWT = errors.New("not a json web token")
ErrEmptyToken = errors.New(jwtErr + "token is empty") ErrEmptyToken = errors.New("token is empty")
ErrUnsupportedAlg = errors.New(jwtErr + "unsupported algorithm") ErrUnsupportedAlg = errors.New("unsupported algorithm")
ErrInvalid = errors.New(jwtErr + "token validation failed") ErrInvalid = errors.New("token validation failed")
ErrBlacklisted = errors.New(jwtErr + "token blacklisted") ErrBlacklisted = errors.New("token blacklisted")
ErrBlacklistNotEnabled = errors.New(jwtErr + "blacklisting is not enabled") ErrBlacklistNotEnabled = errors.New("blacklisting is not enabled")
ErrNbf = errors.New(jwtErr + "token not valid yet") ErrNBF = errors.New("token not valid yet")
ErrExp = errors.New(jwtErr + "token expired") ErrEXP = errors.New("token expired")
ErrMissingNbf = errors.New(jwtErr + "missing nbf claim") ErrMissingEXP = errors.New("missing exp claim")
ErrMissingExp = errors.New(jwtErr + "missing exp claim") ErrMissingTokenParts = errors.New("missing token parts")
ErrMissingTokenParts = errors.New(jwtErr + "missing token parts") ErrEmptySignature = errors.New("token signature is empty")
ErrEmptySignature = errors.New(jwtErr + "token signature is empty") ErrTokenIsNil = errors.New("token is nil")
ErrTokenIsNil = errors.New(jwtErr + "token is nil") ErrInvalidKeySize = errors.New("invalid secret key size")
ErrInvalidKeySize = errors.New(jwtErr + "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 // 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 blacklist Blacklist
nonce bool done chan struct{}
stopOnce sync.Once
done 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 Blacklist, secret io.Reader) (*JWT, error) {
jwt := &JWT{} if expiry <= 0 {
for _, option := range options { expiry = DefaultExpiry
option(jwt)
} }
if jwt.expiry <= 0 { if secret == nil {
jwt.expiry = DefaultExpiry secret = DefaultSecretReader
} }
if jwt.secretReader == nil { secret = io.LimitReader(secret, KeySize)
jwt.secretReader = DefaultSecretReader
}
secret := io.LimitReader(jwt.secretReader, KeySize)
key := make([]byte, KeySize) key := make([]byte, KeySize)
i, err := secret.Read(key) i, err := secret.Read(key)
if err != nil { 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 { if i < KeySize {
return nil, ErrInvalidKeySize return nil, ErrInvalidKeySize
} }
jwt.key = key jwt := &JWT{
if jwt.blacklist != nil { key: key,
expiry: expiry,
blacklist: blacklist,
}
if blacklist != nil {
jwt.done = make(chan struct{}) jwt.done = make(chan struct{})
go jwt.clean() go jwt.clean()
} }
return jwt, nil 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 { func (jwt *JWT) sum(token string, h Hash) []byte {
mac := hmac.New(h.Hash, jwt.key) mac := hmac.New(h.Hash, jwt.key)
mac.Write([]byte(token)) mac.Write([]byte(token))
@ -149,27 +100,28 @@ func (jwt *JWT) Invalidate(t *Token) error {
if t == nil { if t == nil {
return ErrTokenIsNil 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 { if err := jwt.blacklisted(t.Sig()); err != nil {
return err return err
} }
if t.Header.GetString(TypClaim) != Typ { jwt.blacklist.Add(t.Sig(), exp)
return ErrNoJWT return nil
}
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)
} }
// blacklisted checks if a token is blacklisted // blacklisted checks if a token is blacklisted
@ -193,12 +145,8 @@ func (jwt *JWT) clean() {
t.Stop() t.Stop()
return return
} }
now := Now() now := time.Now().UTC().Unix()
m, err := jwt.blacklist.Map() for k, v := range jwt.blacklist.Map() {
if err != nil {
continue
}
for k, v := range m {
if now > v { if now > v {
jwt.blacklist.Remove(k) jwt.blacklist.Remove(k)
} }
@ -207,31 +155,34 @@ func (jwt *JWT) clean() {
} }
// 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 { if t == nil {
return ErrTokenIsNil 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 return ErrNoJWT
} }
h := NewHash(t.Header.GetString(AlgClaim)) var h Hash
if h == nil { switch a := header["alg"].(type) {
case string:
h = ParseHash(a)
if h == nil {
return ErrUnsupportedAlg
}
default:
return ErrUnsupportedAlg return ErrUnsupportedAlg
} }
now := time.Now() now := time.Now().UTC()
if _, ok := t.Claims.Get(ExpClaim); !ok { t.Claims["exp"] = now.Add(jwt.expiry).Unix()
t.Claims.Set(ExpClaim, newExp(now, jwt.expiry)) t.Claims["nbf"] = now.Unix()
} head, err := json.Marshal(header)
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)
if err != nil { if err != nil {
return return
} }
@ -239,10 +190,8 @@ func (jwt *JWT) Sign(t *Token) (err error) {
if err != nil { if err != nil {
return return
} }
t.data = enc.EncodeToString(head) + TokenSeparator + enc.EncodeToString(claims) t.data = strings.Join([]string{base64.URLEncoding.EncodeToString(head), base64.URLEncoding.EncodeToString(claims)}, ".")
t.rawSignature = jwt.sum(t.data, h) t.raw = strings.Join([]string{t.data, base64.URLEncoding.EncodeToString(jwt.sum(t.data, h))}, ".")
t.signature = enc.EncodeToString(t.rawSignature)
t.raw = t.data + TokenSeparator + t.signature
return return
} }
@ -251,32 +200,64 @@ func (jwt *JWT) Verify(t *Token) error {
if t == nil { if t == nil {
return ErrTokenIsNil 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 jwt.blacklist != nil {
if err := jwt.blacklisted(t.Sig()); err != nil { err := jwt.blacklisted(t.Sig())
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)
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()) { if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) {
return ErrInvalid return ErrInvalid
@ -284,13 +265,103 @@ func (jwt *JWT) Verify(t *Token) error {
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 == nil {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
jwt.stopOnce.Do(func() { close(jwt.done)
close(jwt.done)
})
return nil 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,
}
}

View file

@ -1,4 +1,4 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt
@ -9,22 +9,17 @@ import (
"time" "time"
) )
var claims = Claims{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"fizz": "buzz",
}
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
jwt, err := New( jwt, err := New(time.Second, NewMemBlacklist(), nil)
WithExpiry(time.Second),
WithBlacklist(NewMemBlacklist()),
)
if err != nil { if err != nil {
t.Error(err) 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) err = jwt.Sign(token)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -47,21 +42,6 @@ func TestValidate(t *testing.T) {
t.Error(errors.New("token should be expired")) 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) err = jwt.Invalidate(nt)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -70,7 +50,6 @@ func TestValidate(t *testing.T) {
if err == nil { if err == nil {
t.Error(errors.New("double invalidate")) t.Error(errors.New("double invalidate"))
} }
err = jwt.Verify(nt) err = jwt.Verify(nt)
if err == nil { if err == nil {
t.Error(errors.New("token should be blacklisted")) t.Error(errors.New("token should be blacklisted"))
@ -79,46 +58,19 @@ func TestValidate(t *testing.T) {
if err != nil { if err != nil {
t.Error(err) 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) { func TestNoBlacklist(t *testing.T) {
jwt, err := New(WithExpiry(time.Second)) jwt, err := New(time.Second, nil, nil)
if err != nil { if err != nil {
t.Error(err) 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) err = jwt.Sign(token)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -148,103 +100,15 @@ func TestNoBlacklist(t *testing.T) {
} }
func TestEmptySecretReader(t *testing.T) { func TestEmptySecretReader(t *testing.T) {
_, err := New( _, err := New(time.Second, nil, new(bytes.Buffer))
WithExpiry(time.Second),
WithSecret(new(bytes.Buffer)),
)
if err == nil { if err == nil {
t.Error(errors.New("error should be secret reader error")) t.Error(errors.New("error should be secret reader error"))
} }
} }
func TestInvalidSecretReader(t *testing.T) { func TestInvalidSecretReader(t *testing.T) {
_, err := New( _, err := New(time.Second, nil, bytes.NewBufferString("123"))
WithExpiry(time.Second),
WithSecret(bytes.NewBufferString("123")),
)
if err != ErrInvalidKeySize { if err != ErrInvalidKeySize {
t.Error(errors.New("error should be invalid key size")) 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
}