Compare commits

..

No commits in common. "master" and "v3.1.0" have entirely different histories.

10 changed files with 91 additions and 157 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,4 +1,4 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt

View file

@ -1,4 +1,4 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt
@ -30,31 +30,40 @@ func (c Claims) GetBool(key string) (b bool) {
// GetInt returns an int from the claims map // GetInt returns an int from the claims map
func (c Claims) GetInt(key string) (i int) { func (c Claims) GetInt(key string) (i int) {
i = int(c.GetInt64(key)) if f, ok := c.getFloat64(key); ok {
return int(f)
}
if v, ok := c.Get(key); ok && v != nil {
i, _ = v.(int)
}
return return
} }
// GetInt64 returns an int64 from the claims map // GetInt64 returns an int64 from the claims map
func (c Claims) GetInt64(key string) (i int64) { func (c Claims) GetInt64(key string) (i int64) {
if f, ok := c.getFloat64(key); ok {
return int64(f)
}
if v, ok := c.Get(key); ok && v != nil { if v, ok := c.Get(key); ok && v != nil {
switch val := v.(type) { i, _ = v.(int64)
case int64:
i = val
case float64:
i = int64(val)
}
} }
return return
} }
// getFloat64 returns a float64 and ok from the claims map // getFloat64 returns a float64 and ok from the claims map
func (c Claims) GetFloat64(key string) (f float64) { func (c Claims) getFloat64(key string) (f float64, fok bool) {
if v, ok := c.Get(key); ok && v != nil { if v, ok := c.Get(key); ok && v != nil {
f, _ = v.(float64) f, fok = v.(float64)
} }
return return
} }
// GetFloat64 returns a float64 from the claims map
func (c Claims) GetFloat64(key string) (f float64) {
f, _ = c.getFloat64(key)
return
}
// Set sets the value of key in the claims map, if not nil // Set sets the value of key in the claims map, if not nil
func (c Claims) Set(key string, v interface{}) { func (c Claims) Set(key string, v interface{}) {
if c == nil { if c == nil {

View file

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

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

79
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,6 +6,7 @@ package jwt
import ( import (
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
@ -31,8 +32,6 @@ const (
ExpClaim = "exp" ExpClaim = "exp"
// NbfClaim is the nbf claim name // NbfClaim is the nbf claim name
NbfClaim = "nbf" NbfClaim = "nbf"
// NonceClaim
NonceClaim = "nonce"
// TokenSeparator is the tokens separator char // TokenSeparator is the tokens separator char
TokenSeparator = "." TokenSeparator = "."
) )
@ -59,39 +58,14 @@ var (
ErrInvalidKeySize = errors.New(jwtErr + "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 stopOnce sync.Once
stopOnce sync.Once done chan struct{}
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.
@ -100,18 +74,14 @@ type JWT struct {
// If secret is nil the DefaultSecretReader is used. // 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 {
@ -120,8 +90,12 @@ func New(options ...JWTOption) (*JWT, 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()
} }
@ -155,7 +129,7 @@ func (jwt *JWT) Invalidate(t *Token) error {
if t.Header.GetString(TypClaim) != Typ { if t.Header.GetString(TypClaim) != Typ {
return ErrNoJWT return ErrNoJWT
} }
h := NewHash(t.Header.GetString(AlgClaim)) h := ParseHash(t.Header.GetString(AlgClaim))
if h == nil { if h == nil {
return ErrUnsupportedAlg return ErrUnsupportedAlg
} }
@ -217,7 +191,7 @@ func (jwt *JWT) Sign(t *Token) (err error) {
if t.Header.GetString(TypClaim) != Typ { if t.Header.GetString(TypClaim) != Typ {
return ErrNoJWT return ErrNoJWT
} }
h := NewHash(t.Header.GetString(AlgClaim)) h := ParseHash(t.Header.GetString(AlgClaim))
if h == nil { if h == nil {
return ErrUnsupportedAlg return ErrUnsupportedAlg
} }
@ -228,9 +202,6 @@ func (jwt *JWT) Sign(t *Token) (err error) {
if _, ok := t.Claims.Get(NbfClaim); !ok { if _, ok := t.Claims.Get(NbfClaim); !ok {
t.Claims.Set(NbfClaim, NewNbf(now)) t.Claims.Set(NbfClaim, NewNbf(now))
} }
if jwt.nonce {
t.Claims.Set(NonceClaim, enc.EncodeToString(key16()))
}
head, err := json.Marshal(t.Header) head, err := json.Marshal(t.Header)
if err != nil { if err != nil {
return return
@ -239,9 +210,9 @@ 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 = base64.URLEncoding.EncodeToString(head) + TokenSeparator + base64.URLEncoding.EncodeToString(claims)
t.rawSignature = jwt.sum(t.data, h) t.rawSignature = jwt.sum(t.data, h)
t.signature = enc.EncodeToString(t.rawSignature) t.signature = base64.URLEncoding.EncodeToString(t.rawSignature)
t.raw = t.data + TokenSeparator + t.signature t.raw = t.data + TokenSeparator + t.signature
return return
} }
@ -260,7 +231,7 @@ func (jwt *JWT) Verify(t *Token) error {
if t.Header.GetString(TypClaim) != Typ { if t.Header.GetString(TypClaim) != Typ {
return ErrNoJWT return ErrNoJWT
} }
h := NewHash(t.Header.GetString(AlgClaim)) h := ParseHash(t.Header.GetString(AlgClaim))
if h == nil { if h == nil {
return ErrUnsupportedAlg return ErrUnsupportedAlg
} }

View file

@ -1,4 +1,4 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt
@ -17,10 +17,7 @@ var claims = Claims{
} }
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)
} }
@ -86,35 +83,8 @@ func TestValidate(t *testing.T) {
} }
} }
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)
} }
@ -148,27 +118,21 @@ 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) { func BenchmarkDecodeToken(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), nil) t := NewToken(claims.Copy(), nil)
jwt.Sign(t) jwt.Sign(t)
token := t.String() token := t.String()
@ -181,19 +145,19 @@ func BenchmarkDecodeToken(b *testing.B) {
func BenchmarkNew(b *testing.B) { func BenchmarkNew(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
New() New(0, nil, nil)
} }
} }
func BenchmarkNewWithBlacklist(b *testing.B) { func BenchmarkNewWithBlacklist(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
New(WithBlacklist(NewMemBlacklist())) New(0, NewMemBlacklist(), nil)
} }
} }
func BenchmarkSignHS256(b *testing.B) { func BenchmarkSignHS256(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), nil) t := NewToken(claims.Copy(), nil)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -202,8 +166,8 @@ func BenchmarkSignHS256(b *testing.B) {
} }
func BenchmarkSignHS384(b *testing.B) { func BenchmarkSignHS384(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), NewHash(HS384Name)) t := NewToken(claims.Copy(), NewHS384())
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
jwt.Sign(t) jwt.Sign(t)
@ -211,8 +175,8 @@ func BenchmarkSignHS384(b *testing.B) {
} }
func BenchmarkSignHS512(b *testing.B) { func BenchmarkSignHS512(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), NewHash(HS512Name)) t := NewToken(claims.Copy(), NewHS512())
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
jwt.Sign(t) jwt.Sign(t)
@ -220,7 +184,7 @@ func BenchmarkSignHS512(b *testing.B) {
} }
func BenchmarkVerifyHS256(b *testing.B) { func BenchmarkVerifyHS256(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), nil) t := NewToken(claims.Copy(), nil)
jwt.Sign(t) jwt.Sign(t)
b.ReportAllocs() b.ReportAllocs()
@ -230,8 +194,8 @@ func BenchmarkVerifyHS256(b *testing.B) {
} }
func BenchmarkVerifyHS384(b *testing.B) { func BenchmarkVerifyHS384(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), NewHash(HS384Name)) t := NewToken(claims.Copy(), NewHS384())
jwt.Sign(t) jwt.Sign(t)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -240,8 +204,8 @@ func BenchmarkVerifyHS384(b *testing.B) {
} }
func BenchmarkVerifyHS512(b *testing.B) { func BenchmarkVerifyHS512(b *testing.B) {
jwt, _ := New() jwt, _ := New(0, nil, nil)
t := NewToken(claims.Copy(), NewHash(HS512Name)) t := NewToken(claims.Copy(), NewHS512())
jwt.Sign(t) jwt.Sign(t)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

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
}

View file

@ -1,5 +1,3 @@
// Copyright (C) 2025 Marius Schellenberger
package jwt package jwt
import "time" import "time"

View file

@ -1,8 +1,9 @@
// Copyright (C) 2025 Marius Schellenberger // Copyright (C) 2018 Marius Schellenberger
package jwt package jwt
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"strings" "strings"
) )
@ -45,7 +46,7 @@ func NewToken(claims Claims, hash Hash) *Token {
claims = make(Claims) claims = make(Claims)
} }
if hash == nil { if hash == nil {
hash = NewHash(HS256Name) hash = NewHS256()
} }
return &Token{ return &Token{
Header: Claims{ Header: Claims{
@ -74,7 +75,7 @@ func DecodeToken(token string) (t *Token, err error) {
} }
} }
t = new(Token) t = new(Token)
header, err := enc.DecodeString(parts[0]) header, err := base64.URLEncoding.DecodeString(parts[0])
if err != nil { if err != nil {
return return
} }
@ -82,7 +83,7 @@ func DecodeToken(token string) (t *Token, err error) {
if err != nil { if err != nil {
return return
} }
claims, err := enc.DecodeString(parts[1]) claims, err := base64.URLEncoding.DecodeString(parts[1])
if err != nil { if err != nil {
return return
} }
@ -90,7 +91,7 @@ func DecodeToken(token string) (t *Token, err error) {
if err != nil { if err != nil {
return return
} }
t.rawSignature, err = enc.DecodeString(parts[2]) t.rawSignature, err = base64.URLEncoding.DecodeString(parts[2])
t.signature = parts[2] t.signature = parts[2]
t.data = parts[0] + TokenSeparator + parts[1] t.data = parts[0] + TokenSeparator + parts[1]
t.raw = token t.raw = token