Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 473d54569c | |||
| 1613055dfd | |||
| a00a154398 | |||
| 45825d685e | |||
| 4076e5cf17 | |||
| b071c473f0 | |||
| 1bdb6009cf | |||
| fb52ae4a30 | |||
| b4a92fe472 | |||
| f2d57c6e18 | |||
| 826d035d3e | |||
| 6e61945c2c |
12 changed files with 611 additions and 274 deletions
2
LICENSE
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2018 Marius Schellenberger
|
||||
Copyright (C) 2025 Marius Schellenberger
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
|||
62
README.md
62
README.md
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
18
blacklist.go
18
blacklist.go
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2018 Marius Schellenberger
|
||||
// Copyright (C) 2025 Marius Schellenberger
|
||||
|
||||
package jwt
|
||||
|
||||
|
|
@ -6,10 +6,10 @@ import "sync"
|
|||
|
||||
// Blacklist is the blacklisting storage interface
|
||||
type Blacklist interface {
|
||||
Add(string, int64)
|
||||
Remove(string)
|
||||
Add(string, int64) error
|
||||
Remove(string) error
|
||||
Check(string) bool
|
||||
Map() BlacklistMap
|
||||
Map() (BlacklistMap, error)
|
||||
}
|
||||
|
||||
// BlacklistMap is the blacklist map structure
|
||||
|
|
@ -22,23 +22,25 @@ type MemBlacklist struct {
|
|||
list BlacklistMap
|
||||
}
|
||||
|
||||
// NewMemBlacklist
|
||||
// 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) {
|
||||
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) {
|
||||
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
|
||||
|
|
@ -50,7 +52,7 @@ func (mb *MemBlacklist) Check(sig string) (ok bool) {
|
|||
}
|
||||
|
||||
// Map returns the blacklist in the form of a iterable map structure for cleanup
|
||||
func (mb *MemBlacklist) Map() (list BlacklistMap) {
|
||||
func (mb *MemBlacklist) Map() (list BlacklistMap, err error) {
|
||||
list = make(BlacklistMap)
|
||||
mb.RLock()
|
||||
for k, v := range mb.list {
|
||||
|
|
|
|||
78
claims.go
Normal file
78
claims.go
Normal 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
7
encoding.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (C) 2025 Marius Schellenberger
|
||||
|
||||
package jwt
|
||||
|
||||
import "encoding/base64"
|
||||
|
||||
var enc = base64.RawURLEncoding
|
||||
1
go.mod
Normal file
1
go.mod
Normal file
|
|
@ -0,0 +1 @@
|
|||
module git.giftfish.de/ston1th/jwt/v3
|
||||
33
hash.go
33
hash.go
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2018 Marius Schellenberger
|
||||
// Copyright (C) 2025 Marius Schellenberger
|
||||
|
||||
package jwt
|
||||
|
||||
|
|
@ -14,19 +14,25 @@ const (
|
|||
HS512Name = "HS512"
|
||||
)
|
||||
|
||||
// ParseHash returns the Hash type equal to the input string
|
||||
func ParseHash(alg string) Hash {
|
||||
// NewHash returns the Hash type equal to the input string
|
||||
func NewHash(alg string) Hash {
|
||||
switch alg {
|
||||
case HS256Name:
|
||||
return NewHS256()
|
||||
return hs256
|
||||
case HS384Name:
|
||||
return NewHS384()
|
||||
return hs384
|
||||
case HS512Name:
|
||||
return NewHS512()
|
||||
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
|
||||
|
|
@ -36,11 +42,6 @@ type Hash interface {
|
|||
// HS256 implements the Hash interface with SHA256
|
||||
type HS256 struct{}
|
||||
|
||||
// NewHS256 returns a new HS256 instance
|
||||
func NewHS256() Hash {
|
||||
return HS256{}
|
||||
}
|
||||
|
||||
// Alg returns the algorithm name "HS256"
|
||||
func (HS256) Alg() string {
|
||||
return HS256Name
|
||||
|
|
@ -54,11 +55,6 @@ func (HS256) Hash() hash.Hash {
|
|||
// HS384 implements the Hash interface with SHA384
|
||||
type HS384 struct{}
|
||||
|
||||
// NewHS384 returns a new HS384 instance
|
||||
func NewHS384() Hash {
|
||||
return HS384{}
|
||||
}
|
||||
|
||||
// Alg returns the algorithm name "HS384"
|
||||
func (HS384) Alg() string {
|
||||
return HS384Name
|
||||
|
|
@ -72,11 +68,6 @@ func (HS384) Hash() hash.Hash {
|
|||
// HS512 implements the Hash interface with SHA512
|
||||
type HS512 struct{}
|
||||
|
||||
// NewHS512 returns a new HS512 instance
|
||||
func NewHS512() Hash {
|
||||
return HS512{}
|
||||
}
|
||||
|
||||
// Alg returns the algorithm name "HS512"
|
||||
func (HS512) Alg() string {
|
||||
return HS512Name
|
||||
|
|
|
|||
369
jwt.go
369
jwt.go
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2018 Marius Schellenberger
|
||||
// Copyright (C) 2025 Marius Schellenberger
|
||||
|
||||
// Package jwt provides a easy to use JSON Web Token and blacklisting library
|
||||
package jwt
|
||||
|
|
@ -6,85 +6,134 @@ package jwt
|
|||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Header is the default HTTP Authorization header name
|
||||
Header = "Authorization"
|
||||
// HTTPHeader is the default HTTP Authorization header name
|
||||
HTTPHeader = "Authorization"
|
||||
// DefaultExpiry is the default token expiration time
|
||||
DefaultExpiry = time.Hour * 12
|
||||
// KeySize is the secret key size
|
||||
KeySize = 64
|
||||
|
||||
typ = "JWT"
|
||||
// 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
|
||||
var DefaultSecretReader = rand.Reader
|
||||
|
||||
const jwtErr = "jwt: "
|
||||
|
||||
var (
|
||||
ErrNoJWT = errors.New("not a json web token")
|
||||
ErrEmptyToken = errors.New("token is empty")
|
||||
ErrUnsupportedAlg = errors.New("unsupported algorithm")
|
||||
ErrInvalid = errors.New("token validation failed")
|
||||
ErrBlacklisted = errors.New("token blacklisted")
|
||||
ErrBlacklistNotEnabled = errors.New("blacklisting is not enabled")
|
||||
ErrNBF = errors.New("token not valid yet")
|
||||
ErrEXP = errors.New("token expired")
|
||||
ErrMissingEXP = errors.New("missing exp claim")
|
||||
ErrMissingTokenParts = errors.New("missing token parts")
|
||||
ErrEmptySignature = errors.New("token signature is empty")
|
||||
ErrTokenIsNil = errors.New("token is nil")
|
||||
ErrInvalidKeySize = errors.New("invalid secret key size")
|
||||
ErrNoJWT = errors.New(jwtErr + "not a json web token")
|
||||
ErrEmptyToken = errors.New(jwtErr + "token is empty")
|
||||
ErrUnsupportedAlg = errors.New(jwtErr + "unsupported algorithm")
|
||||
ErrInvalid = errors.New(jwtErr + "token validation failed")
|
||||
ErrBlacklisted = errors.New(jwtErr + "token blacklisted")
|
||||
ErrBlacklistNotEnabled = errors.New(jwtErr + "blacklisting is not enabled")
|
||||
ErrNbf = errors.New(jwtErr + "token not valid yet")
|
||||
ErrExp = errors.New(jwtErr + "token expired")
|
||||
ErrMissingNbf = errors.New(jwtErr + "missing nbf claim")
|
||||
ErrMissingExp = errors.New(jwtErr + "missing exp claim")
|
||||
ErrMissingTokenParts = errors.New(jwtErr + "missing token parts")
|
||||
ErrEmptySignature = errors.New(jwtErr + "token signature is empty")
|
||||
ErrTokenIsNil = errors.New(jwtErr + "token is nil")
|
||||
ErrInvalidKeySize = errors.New(jwtErr + "invalid secret key size")
|
||||
)
|
||||
|
||||
type JWTOption func(*JWT)
|
||||
|
||||
func WithExpiry(expiry time.Duration) JWTOption {
|
||||
return func(jwt *JWT) {
|
||||
jwt.expiry = expiry
|
||||
}
|
||||
}
|
||||
|
||||
func WithBlacklist(blacklist Blacklist) JWTOption {
|
||||
return func(jwt *JWT) {
|
||||
jwt.blacklist = blacklist
|
||||
}
|
||||
}
|
||||
func WithSecret(secret io.Reader) JWTOption {
|
||||
return func(jwt *JWT) {
|
||||
jwt.secretReader = secret
|
||||
}
|
||||
}
|
||||
func WithNonce() JWTOption {
|
||||
return func(jwt *JWT) {
|
||||
jwt.nonce = true
|
||||
}
|
||||
}
|
||||
|
||||
// JWT represents the JSON Web Token signing and blacklisting infrastructure
|
||||
type JWT struct {
|
||||
secretReader io.Reader
|
||||
key []byte
|
||||
expiry time.Duration
|
||||
|
||||
blacklist Blacklist
|
||||
nonce bool
|
||||
stopOnce sync.Once
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// New returns a new JWT object with the given expiry timeout.
|
||||
// If the timeout is less or equal to zero the default expiry (12 hours) is used.
|
||||
// The secret key size needs to be at least 64 bytes.
|
||||
// If secret is nil the DefaultSecretReader is used.
|
||||
// If blacklisting is enabled, the JWT object leaks a goroutine to garbage-collect expired blacklisted tokens.
|
||||
// Call the Stop() method to exit the goroutine.
|
||||
func New(expiry time.Duration, blacklist Blacklist, secret io.Reader) (*JWT, error) {
|
||||
if expiry <= 0 {
|
||||
expiry = DefaultExpiry
|
||||
func New(options ...JWTOption) (*JWT, error) {
|
||||
jwt := &JWT{}
|
||||
for _, option := range options {
|
||||
option(jwt)
|
||||
}
|
||||
if secret == nil {
|
||||
secret = DefaultSecretReader
|
||||
if jwt.expiry <= 0 {
|
||||
jwt.expiry = DefaultExpiry
|
||||
}
|
||||
secret = io.LimitReader(secret, KeySize)
|
||||
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 {
|
||||
return nil, errors.New("secret reader error: " + err.Error())
|
||||
return nil, errors.New(jwtErr + "secret reader error: " + err.Error())
|
||||
}
|
||||
if i < KeySize {
|
||||
return nil, ErrInvalidKeySize
|
||||
}
|
||||
jwt := &JWT{
|
||||
key: key,
|
||||
expiry: expiry,
|
||||
blacklist: blacklist,
|
||||
}
|
||||
if blacklist != nil {
|
||||
jwt.key = key
|
||||
if jwt.blacklist != nil {
|
||||
jwt.done = make(chan struct{})
|
||||
go jwt.clean()
|
||||
}
|
||||
return jwt, nil
|
||||
}
|
||||
|
||||
// Expiry returns the configured expiry
|
||||
func (jwt *JWT) Expiry() time.Duration {
|
||||
return jwt.expiry
|
||||
}
|
||||
|
||||
// sum calculates the HMAC hash sum of the token
|
||||
func (jwt *JWT) sum(token string, h Hash) []byte {
|
||||
mac := hmac.New(h.Hash, jwt.key)
|
||||
mac.Write([]byte(token))
|
||||
|
|
@ -100,28 +149,27 @@ func (jwt *JWT) Invalidate(t *Token) error {
|
|||
if t == nil {
|
||||
return ErrTokenIsNil
|
||||
}
|
||||
var (
|
||||
expOK bool
|
||||
exp int64
|
||||
)
|
||||
switch n := t.Claims["exp"].(type) {
|
||||
case json.Number:
|
||||
if i, err := n.Int64(); err == nil {
|
||||
exp = i
|
||||
expOK = true
|
||||
}
|
||||
case float64:
|
||||
exp = int64(n)
|
||||
expOK = true
|
||||
}
|
||||
if !expOK {
|
||||
return ErrMissingEXP
|
||||
}
|
||||
if err := jwt.blacklisted(t.Sig()); err != nil {
|
||||
return err
|
||||
}
|
||||
jwt.blacklist.Add(t.Sig(), exp)
|
||||
return nil
|
||||
if t.Header.GetString(TypClaim) != Typ {
|
||||
return ErrNoJWT
|
||||
}
|
||||
h := NewHash(t.Header.GetString(AlgClaim))
|
||||
if h == nil {
|
||||
return ErrUnsupportedAlg
|
||||
}
|
||||
exp := t.Claims.GetInt64(ExpClaim)
|
||||
switch {
|
||||
case exp == 0:
|
||||
return ErrMissingExp
|
||||
case Now() > exp:
|
||||
return ErrExp
|
||||
}
|
||||
if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) {
|
||||
return ErrInvalid
|
||||
}
|
||||
return jwt.blacklist.Add(t.Sig(), exp)
|
||||
}
|
||||
|
||||
// blacklisted checks if a token is blacklisted
|
||||
|
|
@ -145,8 +193,12 @@ func (jwt *JWT) clean() {
|
|||
t.Stop()
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Unix()
|
||||
for k, v := range jwt.blacklist.Map() {
|
||||
now := Now()
|
||||
m, err := jwt.blacklist.Map()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for k, v := range m {
|
||||
if now > v {
|
||||
jwt.blacklist.Remove(k)
|
||||
}
|
||||
|
|
@ -155,34 +207,31 @@ func (jwt *JWT) clean() {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
if t == nil {
|
||||
return ErrTokenIsNil
|
||||
}
|
||||
header := t.Header()
|
||||
switch t := header["typ"].(type) {
|
||||
case string:
|
||||
if t != typ {
|
||||
if t.Header.GetString(TypClaim) != Typ {
|
||||
return ErrNoJWT
|
||||
}
|
||||
default:
|
||||
return ErrNoJWT
|
||||
}
|
||||
var h Hash
|
||||
switch a := header["alg"].(type) {
|
||||
case string:
|
||||
h = ParseHash(a)
|
||||
h := NewHash(t.Header.GetString(AlgClaim))
|
||||
if h == nil {
|
||||
return ErrUnsupportedAlg
|
||||
}
|
||||
default:
|
||||
return ErrUnsupportedAlg
|
||||
now := time.Now()
|
||||
if _, ok := t.Claims.Get(ExpClaim); !ok {
|
||||
t.Claims.Set(ExpClaim, newExp(now, jwt.expiry))
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
t.Claims["exp"] = now.Add(jwt.expiry).Unix()
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
|
@ -190,8 +239,10 @@ func (jwt *JWT) Sign(t *Token) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.data = strings.Join([]string{base64.URLEncoding.EncodeToString(head), base64.URLEncoding.EncodeToString(claims)}, ".")
|
||||
t.raw = strings.Join([]string{t.data, base64.URLEncoding.EncodeToString(jwt.sum(t.data, h))}, ".")
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -200,64 +251,32 @@ func (jwt *JWT) Verify(t *Token) error {
|
|||
if t == nil {
|
||||
return ErrTokenIsNil
|
||||
}
|
||||
now := time.Now().UTC().Unix()
|
||||
var (
|
||||
exp int64
|
||||
nbf int64
|
||||
expOK bool
|
||||
nbfOK bool
|
||||
h Hash
|
||||
)
|
||||
header := t.Header()
|
||||
switch t := header["typ"].(type) {
|
||||
case string:
|
||||
if t != typ {
|
||||
return ErrNoJWT
|
||||
}
|
||||
default:
|
||||
return ErrNoJWT
|
||||
}
|
||||
switch a := header["alg"].(type) {
|
||||
case string:
|
||||
h = ParseHash(a)
|
||||
if h == nil {
|
||||
return ErrUnsupportedAlg
|
||||
}
|
||||
default:
|
||||
return ErrUnsupportedAlg
|
||||
}
|
||||
|
||||
switch n := t.Claims["exp"].(type) {
|
||||
case json.Number:
|
||||
if i, err := n.Int64(); err == nil {
|
||||
exp = i
|
||||
expOK = true
|
||||
}
|
||||
case float64:
|
||||
exp = int64(n)
|
||||
expOK = true
|
||||
}
|
||||
switch n := t.Claims["nbf"].(type) {
|
||||
case json.Number:
|
||||
if i, err := n.Int64(); err == nil {
|
||||
nbf = i
|
||||
nbfOK = true
|
||||
}
|
||||
case float64:
|
||||
nbf = int64(n)
|
||||
nbfOK = true
|
||||
}
|
||||
if jwt.blacklist != nil {
|
||||
err := jwt.blacklisted(t.Sig())
|
||||
if err != nil {
|
||||
if err := jwt.blacklisted(t.Sig()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if expOK && now > exp {
|
||||
return ErrEXP
|
||||
now := Now()
|
||||
if t.Header.GetString(TypClaim) != Typ {
|
||||
return ErrNoJWT
|
||||
}
|
||||
if nbfOK && now < nbf {
|
||||
return ErrNBF
|
||||
h := NewHash(t.Header.GetString(AlgClaim))
|
||||
if h == nil {
|
||||
return ErrUnsupportedAlg
|
||||
}
|
||||
exp := t.Claims.GetInt64(ExpClaim)
|
||||
switch {
|
||||
case exp == 0:
|
||||
return ErrMissingExp
|
||||
case now > exp:
|
||||
return ErrExp
|
||||
}
|
||||
nbf := t.Claims.GetInt64(NbfClaim)
|
||||
switch {
|
||||
case nbf == 0:
|
||||
return ErrMissingNbf
|
||||
case now < nbf:
|
||||
return ErrNbf
|
||||
}
|
||||
if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) {
|
||||
return ErrInvalid
|
||||
|
|
@ -265,103 +284,13 @@ func (jwt *JWT) Verify(t *Token) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Stop will end the cleaner goroutinge execution
|
||||
// Calling Stop twice or more will panic
|
||||
// Stop will end the cleaner goroutine execution
|
||||
func (jwt *JWT) Stop() error {
|
||||
if jwt.blacklist == nil {
|
||||
return ErrBlacklistNotEnabled
|
||||
}
|
||||
jwt.stopOnce.Do(func() {
|
||||
close(jwt.done)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeToken decodes a raw string token into a *Token object
|
||||
func DecodeToken(token string) (t *Token, err error) {
|
||||
if token == "" {
|
||||
return nil, ErrEmptyToken
|
||||
}
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) < 3 {
|
||||
return nil, ErrMissingTokenParts
|
||||
}
|
||||
if len(parts) > 3 {
|
||||
return nil, ErrNoJWT
|
||||
}
|
||||
for _, v := range parts {
|
||||
if v == "" {
|
||||
return nil, ErrNoJWT
|
||||
}
|
||||
}
|
||||
t = new(Token)
|
||||
header, err := base64.URLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(header, &t.header)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
claims, err := base64.URLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(claims, &t.Claims)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.rawSignature, err = base64.URLEncoding.DecodeString(parts[2])
|
||||
t.signature = parts[2]
|
||||
t.data = strings.Join(parts[:2], ".")
|
||||
t.raw = token
|
||||
return
|
||||
}
|
||||
|
||||
// Token is the JWT token representation
|
||||
type Token struct {
|
||||
raw string
|
||||
data string
|
||||
signature string
|
||||
rawSignature []byte
|
||||
header map[string]interface{}
|
||||
Claims map[string]interface{}
|
||||
}
|
||||
|
||||
// String returns the tokens encoded string
|
||||
func (t *Token) String() string {
|
||||
return t.raw
|
||||
}
|
||||
|
||||
// Sig returns the Tokens URLEncoded signature
|
||||
func (t *Token) Sig() string {
|
||||
return t.signature
|
||||
}
|
||||
|
||||
// RawSig returns the Tokens raw signature
|
||||
func (t *Token) RawSig() []byte {
|
||||
return t.rawSignature
|
||||
}
|
||||
|
||||
// Header returns the Tokens header
|
||||
func (t *Token) Header() map[string]interface{} {
|
||||
return t.header
|
||||
}
|
||||
|
||||
// Data returns the first two token fields
|
||||
func (t *Token) Data() string {
|
||||
return t.data
|
||||
}
|
||||
|
||||
// NewToken returns a new *Token using the provided hash algorithm and claims
|
||||
// If h is nil HS256 will be used
|
||||
func NewToken(claims map[string]interface{}, h Hash) *Token {
|
||||
if h == nil {
|
||||
h = NewHS256()
|
||||
}
|
||||
return &Token{
|
||||
header: map[string]interface{}{
|
||||
"alg": h.Alg(),
|
||||
"typ": typ,
|
||||
},
|
||||
Claims: claims,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
176
jwt_test.go
176
jwt_test.go
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2018 Marius Schellenberger
|
||||
// Copyright (C) 2025 Marius Schellenberger
|
||||
|
||||
package jwt
|
||||
|
||||
|
|
@ -9,17 +9,22 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
jwt, err := New(time.Second, NewMemBlacklist(), nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
token := NewToken(map[string]interface{}{
|
||||
var claims = Claims{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"admin": true,
|
||||
"fizz": "buzz",
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
jwt, err := New(
|
||||
WithExpiry(time.Second),
|
||||
WithBlacklist(NewMemBlacklist()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
token := NewToken(claims.Copy(), nil)
|
||||
err = jwt.Sign(token)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
|
@ -42,6 +47,21 @@ func TestValidate(t *testing.T) {
|
|||
t.Error(errors.New("token should be expired"))
|
||||
}
|
||||
|
||||
err = jwt.Invalidate(nt)
|
||||
if err == nil {
|
||||
t.Error(errors.New("token is expired"))
|
||||
}
|
||||
|
||||
nt.Claims.Delete(ExpClaim)
|
||||
err = jwt.Sign(nt)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = jwt.Verify(nt)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = jwt.Invalidate(nt)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
|
@ -50,6 +70,7 @@ func TestValidate(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Error(errors.New("double invalidate"))
|
||||
}
|
||||
|
||||
err = jwt.Verify(nt)
|
||||
if err == nil {
|
||||
t.Error(errors.New("token should be blacklisted"))
|
||||
|
|
@ -58,19 +79,46 @@ func TestValidate(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoBlacklist(t *testing.T) {
|
||||
jwt, err := New(time.Second, nil, nil)
|
||||
// should not panic
|
||||
err = jwt.Stop()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
token := NewToken(map[string]interface{}{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"admin": true,
|
||||
"fizz": "buzz",
|
||||
}, nil)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -100,15 +148,103 @@ func TestNoBlacklist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEmptySecretReader(t *testing.T) {
|
||||
_, err := New(time.Second, nil, new(bytes.Buffer))
|
||||
_, 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(time.Second, nil, bytes.NewBufferString("123"))
|
||||
_, 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
11
rand.go
Normal 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
24
time.go
Normal 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
98
token.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue