package jwtutil
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrEmptyKeySet = errors.New("jwtutil: key set is empty")
ErrDuplicateKeyID = errors.New("jwtutil: duplicate key ID")
ErrUnknownKeyID = errors.New("jwtutil: unknown signing key")
ErrSigningMethod = errors.New("jwtutil: unsupported signing method")
)
type HMACKey struct {
ID string
Key []byte
}
func HMACKeyID(key []byte) string {
h := sha256.Sum256(key)
return base64.RawStdEncoding.EncodeToString(h[:8])
}
type HMACKeySet struct {
method jwt.SigningMethod
keys []HMACKey
byID map[string][]byte
}
func NewHMACKeySet(method jwt.SigningMethod, keys ...HMACKey) (*HMACKeySet, error) {
switch method {
case jwt.SigningMethodHS256, jwt.SigningMethodHS384, jwt.SigningMethodHS512:
default:
return nil, fmt.Errorf("%w: %q", ErrSigningMethod, method.Alg())
}
if len(keys) == 0 {
return nil, ErrEmptyKeySet
}
ks := &HMACKeySet{
method: method,
keys: make([]HMACKey, len(keys)),
byID: make(map[string][]byte, len(keys)),
}
for i, k := range keys {
if _, exists := ks.byID[k.ID]; exists {
return nil, fmt.Errorf("%w: %q", ErrDuplicateKeyID, k.ID)
}
ks.keys[i] = k
ks.byID[k.ID] = k.Key
}
return ks, nil
}
func NewHMACKeySetFromBytes(method jwt.SigningMethod, keys ...[]byte) (*HMACKeySet, error) {
hmacKeys := make([]HMACKey, len(keys))
for i, k := range keys {
hmacKeys[i] = HMACKey{ID: HMACKeyID(k), Key: k}
}
return NewHMACKeySet(method, hmacKeys...)
}
func (ks *HMACKeySet) SigningMethod() jwt.SigningMethod {
return ks.method
}
func (ks *HMACKeySet) Sign(claims jwt.Claims) (string, error) {
active := ks.keys[0]
tok := jwt.NewWithClaims(ks.method, claims)
tok.Header["kid"] = active.ID
return tok.SignedString(active.Key)
}
func (ks *HMACKeySet) ParseWithClaims(token string, claims jwt.Claims, opts ...jwt.ParserOption) (*jwt.Token, error) {
allOpts := make([]jwt.ParserOption, 0, len(opts)+1)
allOpts = append(allOpts, jwt.WithValidMethods([]string{ks.method.Alg()}))
allOpts = append(allOpts, opts...)
return jwt.ParseWithClaims(token, claims, ks.keyFunc, allOpts...)
}
func (ks *HMACKeySet) keyFunc(tok *jwt.Token) (any, error) {
kid, _ := tok.Header["kid"].(string)
key, ok := ks.byID[kid]
if !ok {
return nil, fmt.Errorf("%w: %q", ErrUnknownKeyID, kid)
}
return key, nil
}