package jwtutil_test
import (
"crypto/sha256"
"encoding/base64"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/utils/jwtutil"
)
func TestHMACKeyID_StableSHA256First8(t *testing.T) {
key := []byte("the-secret")
want := func() string {
h := sha256.Sum256(key)
return base64.RawStdEncoding.EncodeToString(h[:8])
}()
require.Equal(t, want, jwtutil.HMACKeyID(key))
}
func TestNewHMACKeySet_ValidatesMethod(t *testing.T) {
_, err := jwtutil.NewHMACKeySet(jwt.SigningMethodRS256, jwtutil.HMACKey{ID: "a", Key: []byte("k")})
require.ErrorIs(t, err, jwtutil.ErrSigningMethod)
}
func TestNewHMACKeySet_RejectsEmpty(t *testing.T) {
_, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256)
require.ErrorIs(t, err, jwtutil.ErrEmptyKeySet)
}
func TestNewHMACKeySet_RejectsDuplicateID(t *testing.T) {
_, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256,
jwtutil.HMACKey{ID: "x", Key: []byte("k1")},
jwtutil.HMACKey{ID: "x", Key: []byte("k2")},
)
require.ErrorIs(t, err, jwtutil.ErrDuplicateKeyID)
}
func TestSignVerifyRoundTrip(t *testing.T) {
ks, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, jwtutil.HMACKey{ID: "a", Key: []byte("secret-a")})
require.NoError(t, err)
claims := jwt.RegisteredClaims{
Issuer: "tester",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
}
tokStr, err := ks.Sign(&claims)
require.NoError(t, err)
out := &jwt.RegisteredClaims{}
parsed, err := ks.ParseWithClaims(tokStr, out)
require.NoError(t, err)
require.Equal(t, "a", parsed.Header["kid"])
require.Equal(t, "tester", out.Issuer)
}
func TestRotation_NewSetAcceptsOldKID(t *testing.T) {
keyA := jwtutil.HMACKey{ID: "a", Key: []byte("secret-a")}
keyB := jwtutil.HMACKey{ID: "b", Key: []byte("secret-b")}
signer, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, keyA, keyB)
require.NoError(t, err)
tokStr, err := signer.Sign(&jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
require.NoError(t, err)
verifier, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, keyB, keyA)
require.NoError(t, err)
out := &jwt.RegisteredClaims{}
_, err = verifier.ParseWithClaims(tokStr, out)
require.NoError(t, err)
}
func TestRotation_OldVerifierRejectsNewKID(t *testing.T) {
keyA := jwtutil.HMACKey{ID: "a", Key: []byte("secret-a")}
keyB := jwtutil.HMACKey{ID: "b", Key: []byte("secret-b")}
signer, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, keyB)
require.NoError(t, err)
tokStr, err := signer.Sign(&jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
require.NoError(t, err)
verifier, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, keyA)
require.NoError(t, err)
_, err = verifier.ParseWithClaims(tokStr, &jwt.RegisteredClaims{})
require.Error(t, err)
require.ErrorIs(t, err, jwtutil.ErrUnknownKeyID)
}
func TestVerify_RejectsTokenSignedWithWrongKeyBytes(t *testing.T) {
signer, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, jwtutil.HMACKey{ID: "a", Key: []byte("attacker-key")})
require.NoError(t, err)
tokStr, err := signer.Sign(&jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
require.NoError(t, err)
verifier, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, jwtutil.HMACKey{ID: "a", Key: []byte("real-key")})
require.NoError(t, err)
_, err = verifier.ParseWithClaims(tokStr, &jwt.RegisteredClaims{})
require.Error(t, err)
require.ErrorIs(t, err, jwt.ErrSignatureInvalid)
}
func TestVerify_EnforcesSigningMethod(t *testing.T) {
key := jwtutil.HMACKey{ID: "a", Key: []byte("secret")}
tok512 := jwt.NewWithClaims(jwt.SigningMethodHS512, &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
tok512.Header["kid"] = key.ID
tokStr, err := tok512.SignedString(key.Key)
require.NoError(t, err)
verifier, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, key)
require.NoError(t, err)
_, err = verifier.ParseWithClaims(tokStr, &jwt.RegisteredClaims{})
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "signing method"), "got %v", err)
}
func TestParseWithClaims_ExtraOptionsApplied(t *testing.T) {
key := jwtutil.HMACKey{ID: "a", Key: []byte("secret")}
ks, err := jwtutil.NewHMACKeySet(jwt.SigningMethodHS256, key)
require.NoError(t, err)
tokStr, err := ks.Sign(&jwt.RegisteredClaims{
Issuer: "expected-issuer",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
require.NoError(t, err)
_, err = ks.ParseWithClaims(tokStr, &jwt.RegisteredClaims{}, jwt.WithIssuer("not-this-issuer"))
require.Error(t, err)
require.ErrorIs(t, err, jwt.ErrTokenInvalidIssuer)
}
func TestNewHMACKeySetFromBytes_DerivesIDs(t *testing.T) {
ks, err := jwtutil.NewHMACKeySetFromBytes(jwt.SigningMethodHS256, []byte("k1"), []byte("k2"))
require.NoError(t, err)
tokStr, err := ks.Sign(&jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)),
})
require.NoError(t, err)
parsed, err := ks.ParseWithClaims(tokStr, &jwt.RegisteredClaims{})
require.NoError(t, err)
require.Equal(t, jwtutil.HMACKeyID([]byte("k1")), parsed.Header["kid"])
}