Compare commits

...

21 commits

Author SHA1 Message Date
473d54569c added config options and nonce 2025-10-15 21:49:13 +02:00
1613055dfd use raw base64 as defined by rfc7515 2018-12-28 19:10:22 +01:00
a00a154398 added errors to blacklist 2018-09-22 16:34:10 +02:00
45825d685e added go.mod 2018-09-19 19:42:25 +02:00
4076e5cf17 fixed typos, added error prefix and added usage to README.md 2018-09-13 16:29:45 +02:00
b071c473f0 cleanup 2018-09-13 16:02:17 +02:00
1bdb6009cf added utc time functions 2018-09-13 15:58:24 +02:00
fb52ae4a30 added benchmarks 2018-09-13 14:18:11 +02:00
b4a92fe472 custom exp and nbf 2018-09-13 13:27:32 +02:00
f2d57c6e18 major refactoring 2018-09-13 11:39:49 +02:00
826d035d3e split up library and removed Stop() panic when called twice 2018-09-12 23:35:03 +02:00
6e61945c2c added claims type 2018-09-12 23:14:09 +02:00
8e361f3156 small bug fix 2018-01-17 22:42:11 +01:00
9b4adfe863 moved sync out of JWT and added new secret reader 2018-01-17 22:33:58 +01:00
4c14116a41 added blacklist storage interface 2018-01-17 21:53:53 +01:00
339d6de8bd added empty token check and copyright 2018-01-03 23:04:17 +01:00
40e1f1bf58 fixed typo 2018-01-03 22:56:25 +01:00
9291fdbf6a better error message 2018-01-03 21:17:56 +01:00
fa7ac67fe5 fixed typo 2016-12-29 11:16:33 +01:00
40fe9931a8 rewrite and new Hash interface 2016-12-29 11:08:25 +01:00
e75e02ab44 fixed cleanup deadlock 2016-09-14 11:45:12 +02:00
12 changed files with 851 additions and 255 deletions

View file

@ -1,4 +1,4 @@
Copyright (C) 2016 Marius Schellenberger Copyright (C) 2025 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 +1,61 @@
# jwt - a simple JSON Web Token library in Go # 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)
}
}
```

63
blacklist.go Normal file
View file

@ -0,0 +1,63 @@
// 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
}

78
claims.go Normal file
View file

@ -0,0 +1,78 @@
// 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
}

7
encoding.go Normal file
View file

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

1
go.mod Normal file
View file

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

79
hash.go Normal file
View file

@ -0,0 +1,79 @@
// 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()
}

428
jwt.go
View file

@ -1,118 +1,141 @@
// 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"
"hash" "io"
"strings"
"sync" "sync"
"time" "time"
) )
const ( const (
// Header is the default HTTP Authorization header name // HTTPHeader is the default HTTP Authorization header name
Header = "Authorization" HTTPHeader = "Authorization"
// DefaultExpiry is the default token expiration time // DefaultExpiry is the default token expiration time
DefaultExpiry = time.Hour * 12 DefaultExpiry = time.Hour * 12
typ = "JWT" // KeySize is the secret key size
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 = "."
) )
// Hash represents the three diffrernt hash types // DefaultSecretReader is the default secret key generator
type Hash int var DefaultSecretReader = rand.Reader
const ( const jwtErr = "jwt: "
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("no json web token") ErrNoJWT = errors.New(jwtErr + "not a json web token")
ErrUnsupportedAlg = errors.New("unsupported algoritm") ErrEmptyToken = errors.New(jwtErr + "token is empty")
ErrInvalid = errors.New("token validation failed") ErrUnsupportedAlg = errors.New(jwtErr + "unsupported algorithm")
ErrBlacklisted = errors.New("token blacklisted") ErrInvalid = errors.New(jwtErr + "token validation failed")
ErrBlacklistNotEnabled = errors.New("blacklisting is not enabled") ErrBlacklisted = errors.New(jwtErr + "token blacklisted")
ErrNBF = errors.New("token not valid yet") ErrBlacklistNotEnabled = errors.New(jwtErr + "blacklisting is not enabled")
ErrEXP = errors.New("token expired") ErrNbf = errors.New(jwtErr + "token not valid yet")
ErrMissingEXP = errors.New("missing exp claim") ErrExp = errors.New(jwtErr + "token expired")
ErrMissingTokenParts = errors.New("missíng token parts") ErrMissingNbf = errors.New(jwtErr + "missing nbf claim")
ErrEmptySignature = errors.New("token signature is empty") 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")
) )
// parseHash returns the hash.Hash type equal to the input string type JWTOption func(*JWT)
func parseHash(alg string) (h func() hash.Hash) {
switch alg { func WithExpiry(expiry time.Duration) JWTOption {
case "HS256": return func(jwt *JWT) {
h = sha256.New jwt.expiry = expiry
case "HS384": }
h = sha512.New384 }
case "HS512":
h = sha512.New 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
} }
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 {
key []byte secretReader io.Reader
expiry time.Duration key []byte
expiry time.Duration
// protects list blacklist Blacklist
sync.RWMutex nonce bool
blacklist bool stopOnce sync.Once
list map[string]int64 done chan struct{}
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(expiry time.Duration, blacklist bool) (*JWT, error) { func New(options ...JWTOption) (*JWT, error) {
if expiry <= 0 { jwt := &JWT{}
expiry = DefaultExpiry for _, option := range options {
option(jwt)
} }
key := make([]byte, keySize) if jwt.expiry <= 0 {
_, err := rand.Read(key) jwt.expiry = DefaultExpiry
}
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, err return nil, errors.New(jwtErr + "secret reader error: " + err.Error())
} }
jwt := &JWT{ if i < KeySize {
key: key, return nil, ErrInvalidKeySize
expiry: expiry,
blacklist: blacklist,
} }
if blacklist { jwt.key = key
jwt.list = make(map[string]int64) if jwt.blacklist != nil {
jwt.stop = make(chan struct{}) jwt.done = make(chan struct{})
go jwt.clean() go jwt.clean()
} }
return jwt, nil return jwt, nil
} }
func (jwt *JWT) sum(token string, h func() hash.Hash) []byte { // Expiry returns the configured expiry
mac := hmac.New(h, jwt.key) 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)) mac.Write([]byte(token))
return mac.Sum(nil) return mac.Sum(nil)
} }
@ -120,33 +143,33 @@ func (jwt *JWT) sum(token string, h func() hash.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 { if jwt.blacklist == nil {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
var ( if t == nil {
expOK bool return ErrTokenIsNil
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 { if err := jwt.blacklisted(t.Sig()); err != nil {
return ErrMissingEXP
}
if err := jwt.blacklisted(t.Signature); err != nil {
return err return err
} }
jwt.Lock() if t.Header.GetString(TypClaim) != Typ {
defer jwt.Unlock() return ErrNoJWT
jwt.list[t.Signature] = exp }
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
@ -154,193 +177,120 @@ func (jwt *JWT) blacklisted(sig string) error {
if sig == "" { if sig == "" {
return ErrEmptySignature return ErrEmptySignature
} }
jwt.RLock() if jwt.blacklist.Check(sig) {
defer jwt.RUnlock()
_, ok := jwt.list[sig]
if ok {
return ErrBlacklisted return ErrBlacklisted
} }
return nil return nil
} }
// clean will look for expired blacklisted tokens and removes them // clean looks 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.stop: case <-jwt.done:
t.Stop() t.Stop()
return return
} }
now := time.Now().UTC().Unix() now := Now()
jwt.Lock() m, err := jwt.blacklist.Map()
defer jwt.Unlock() if err != nil {
for k, v := range jwt.list { continue
}
for k, v := range m {
if now > v { if now > v {
delete(jwt.list, k) jwt.blacklist.Remove(k)
} }
} }
} }
} }
// Sign will sign the provided token using the secret key // Sign will sign the provided token using the secret key
// This will overwrite existing 'exp' and 'nbf' claims // 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)
func (jwt *JWT) Sign(t *Token) (err error) { func (jwt *JWT) Sign(t *Token) (err error) {
now := time.Now().UTC() if t == nil {
t.Claims["exp"] = now.Add(jwt.expiry).Unix() return ErrTokenIsNil
t.Claims["nbf"] = now.Unix()
h, err := json.Marshal(t.Header)
if err != nil {
return
} }
c, err := json.Marshal(t.Claims) if t.Header.GetString(TypClaim) != Typ {
if err != nil { return ErrNoJWT
return
} }
var hf func() hash.Hash h := NewHash(t.Header.GetString(AlgClaim))
switch a := t.Header["alg"].(type) { if h == nil {
case string:
hf = parseHash(a)
if hf == nil {
return ErrUnsupportedAlg
}
default:
return ErrUnsupportedAlg return ErrUnsupportedAlg
} }
t.Data = strings.Join([]string{base64.URLEncoding.EncodeToString(h), base64.URLEncoding.EncodeToString(c)}, ".") now := time.Now()
t.Raw = strings.Join([]string{t.Data, base64.URLEncoding.EncodeToString(jwt.sum(t.Data, hf))}, ".") 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)
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 {
now := time.Now().UTC().Unix() if t == nil {
var ( return ErrTokenIsNil
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
}
default:
return ErrNoJWT
} }
switch a := t.Header["alg"].(type) { if jwt.blacklist != nil {
case string: if err := jwt.blacklisted(t.Sig()); err != nil {
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
} }
} }
if expOK && now > exp { now := Now()
return ErrEXP if t.Header.GetString(TypClaim) != Typ {
return ErrNoJWT
} }
if nbfOK && now < nbf { h := NewHash(t.Header.GetString(AlgClaim))
return ErrNBF if h == nil {
return ErrUnsupportedAlg
} }
if !hmac.Equal(jwt.sum(t.Data, hf), t.RawSignature) { 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()) {
return ErrInvalid return ErrInvalid
} }
return nil return nil
} }
// Stop will end the cleaner goroutinge execution // Stop will end the cleaner goroutine execution
// Calling Stop twice or more will panic
func (jwt *JWT) Stop() error { func (jwt *JWT) Stop() error {
if !jwt.blacklist { if jwt.blacklist == nil {
return ErrBlacklistNotEnabled return ErrBlacklistNotEnabled
} }
select { jwt.stopOnce.Do(func() {
case jwt.stop <- struct{}{}: close(jwt.done)
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,25 +1,250 @@
// Copyright (C) 2025 Marius Schellenberger
package jwt package jwt
import ( import (
"bytes"
"errors"
"testing" "testing"
"time" "time"
) )
func TestValidate(t *testing.T) { var claims = Claims{
jwt, _ := New(0, true) "sub": "1234567890",
token := NewToken(HS256, map[string]interface{}{ "name": "John Doe",
"sub": "1234567890", "admin": true,
"name": "John Doe", "fizz": "buzz",
"admin": true, }
"fizz": "buzz",
}) func TestValidate(t *testing.T) {
err := jwt.Sign(token) jwt, err := New(
t.Log(err, token) WithExpiry(time.Second),
nt, err := DecodeToken(token.Raw) WithBlacklist(NewMemBlacklist()),
t.Log(nt, err) )
time.Sleep(time.Second) if err != nil {
t.Log(jwt.Verify(nt)) t.Error(err)
jwt.Invalidate(nt) }
t.Log(jwt.Verify(nt)) token := NewToken(claims.Copy(), nil)
jwt.Stop() 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 Normal file
View file

@ -0,0 +1,11 @@
// 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 Normal file
View file

@ -0,0 +1,24 @@
// 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()
}

98
token.go Normal file
View file

@ -0,0 +1,98 @@
// 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
}