mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 06:55:00 +00:00
795 lines
19 KiB
Go
795 lines
19 KiB
Go
// Copyright 2026 AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
//
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package jwt
|
|
|
|
import (
|
|
"crypto"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func makeHMACSigner(tb testing.TB, hash crypto.Hash, secret []byte) *HMACSigner {
|
|
tb.Helper()
|
|
key, err := NewSharedSecret(secret, "")
|
|
if err != nil {
|
|
tb.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
signer, err := NewHMACSigner(hash, key)
|
|
if err != nil {
|
|
tb.Fatalf("NewHMACSigner: %v", err)
|
|
}
|
|
return signer
|
|
}
|
|
|
|
func makeHMACVerifier(tb testing.TB, hash crypto.Hash, secret []byte) *HMACVerifier {
|
|
tb.Helper()
|
|
key, err := NewSharedSecret(secret, "")
|
|
if err != nil {
|
|
tb.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
verifier, err := NewHMACVerifier(hash, key)
|
|
if err != nil {
|
|
tb.Fatalf("NewHMACVerifier: %v", err)
|
|
}
|
|
return verifier
|
|
}
|
|
|
|
func TestHMACSigner_SignToString(t *testing.T) {
|
|
secret := []byte("test-secret-that-is-long-enough-for-hs256")
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test-issuer",
|
|
Sub: "user-42",
|
|
Aud: Listish{"api"},
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
IAt: time.Now().Unix(),
|
|
}
|
|
|
|
token, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
// Verify structure: three base64url segments
|
|
parts := 0
|
|
for _, c := range token {
|
|
if c == '.' {
|
|
parts++
|
|
}
|
|
}
|
|
if parts != 2 {
|
|
t.Errorf("expected 2 dots in token, got %d", parts)
|
|
}
|
|
|
|
// Verify we can decode and verify
|
|
verifier := signer.Verifier()
|
|
jws, err := verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT: %v", err)
|
|
}
|
|
|
|
var decoded TokenClaims
|
|
if err := jws.UnmarshalClaims(&decoded); err != nil {
|
|
t.Fatalf("UnmarshalClaims: %v", err)
|
|
}
|
|
|
|
if decoded.Iss != "test-issuer" {
|
|
t.Errorf("iss = %q, want %q", decoded.Iss, "test-issuer")
|
|
}
|
|
if decoded.Sub != "user-42" {
|
|
t.Errorf("sub = %q, want %q", decoded.Sub, "user-42")
|
|
}
|
|
}
|
|
|
|
func TestHMACSigner_Sign(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
jws, err := signer.Sign(claims)
|
|
if err != nil {
|
|
t.Fatalf("Sign: %v", err)
|
|
}
|
|
|
|
if jws.GetHeader().Alg != "HS256" {
|
|
t.Errorf("alg = %q, want %q", jws.GetHeader().Alg, "HS256")
|
|
}
|
|
|
|
// KID is empty because the key has no KID set
|
|
if jws.GetHeader().KID != "" {
|
|
t.Errorf("KID = %q, want empty", jws.GetHeader().KID)
|
|
}
|
|
|
|
if len(jws.GetSignature()) == 0 {
|
|
t.Error("signature is empty")
|
|
}
|
|
|
|
token, err := jws.Encode()
|
|
if err != nil {
|
|
t.Fatalf("Encode: %v", err)
|
|
}
|
|
|
|
verifier := signer.Verifier()
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHMACSigner_SignJWT(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
jws, err := New(claims)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
|
|
// Header should be empty initially
|
|
if jws.GetHeader().Alg != "" {
|
|
t.Errorf("initial alg = %q, want empty", jws.GetHeader().Alg)
|
|
}
|
|
|
|
if err := signer.SignJWT(jws); err != nil {
|
|
t.Fatalf("SignJWT: %v", err)
|
|
}
|
|
|
|
if jws.GetHeader().Alg != "HS256" {
|
|
t.Errorf("alg = %q, want %q", jws.GetHeader().Alg, "HS256")
|
|
}
|
|
|
|
// KID is empty because the key has no KID set
|
|
if jws.GetHeader().KID != "" {
|
|
t.Errorf("KID = %q, want empty", jws.GetHeader().KID)
|
|
}
|
|
|
|
if len(jws.GetSignature()) == 0 {
|
|
t.Error("signature is empty after signing")
|
|
}
|
|
}
|
|
|
|
func TestHMACSigner_KID(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
key := newSharedSecret(t, secret, "my-hmac-key")
|
|
signer, err := NewHMACSigner(crypto.SHA256, key)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACSigner: %v", err)
|
|
}
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
jws, err := signer.Sign(claims)
|
|
if err != nil {
|
|
t.Fatalf("Sign: %v", err)
|
|
}
|
|
|
|
if jws.GetHeader().KID != "my-hmac-key" {
|
|
t.Errorf("KID = %q, want %q", jws.GetHeader().KID, "my-hmac-key")
|
|
}
|
|
if jws.GetHeader().Alg != "HS256" {
|
|
t.Errorf("alg = %q, want %q", jws.GetHeader().Alg, "HS256")
|
|
}
|
|
|
|
// Verify round-trip works
|
|
token, encErr := jws.Encode()
|
|
if encErr != nil {
|
|
t.Fatalf("Encode: %v", encErr)
|
|
}
|
|
verifier := signer.Verifier()
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHMACSigner_AlgConflict(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
jws, err := New(claims)
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
|
|
// Set a conflicting algorithm
|
|
hdr := jws.GetHeader()
|
|
hdr.Alg = "HS384"
|
|
if err := jws.SetHeader(&hdr); err != nil {
|
|
t.Fatalf("SetHeader: %v", err)
|
|
}
|
|
|
|
err = signer.SignJWT(jws)
|
|
if err == nil {
|
|
t.Fatal("expected error for alg conflict, got nil")
|
|
}
|
|
if err != ErrAlgConflict {
|
|
t.Errorf("error = %v, want %v", err, ErrAlgConflict)
|
|
}
|
|
}
|
|
|
|
func TestNewHMACSigner_NilSecret(t *testing.T) {
|
|
_, err := NewHMACSigner(crypto.SHA256, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil secret, got nil")
|
|
}
|
|
if !errors.Is(err, ErrNoSigningKey) {
|
|
t.Errorf("error = %v, want %v", err, ErrNoSigningKey)
|
|
}
|
|
}
|
|
|
|
func TestNewHMACSigner_InvalidAlgorithm(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
key, err := NewSharedSecret(secret, "")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
_, err = NewHMACSigner(crypto.Hash(99), key)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid algorithm, got nil")
|
|
}
|
|
}
|
|
|
|
func TestHMACSigner_Verifier(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
verifier := signer.Verifier()
|
|
|
|
if verifier.alg != crypto.SHA256 {
|
|
t.Errorf("verifier alg = %v, want %v", verifier.alg, crypto.SHA256)
|
|
}
|
|
}
|
|
|
|
func TestHMACSigner_SignRaw(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
|
|
// Test with a simple header
|
|
hdr := &RFCHeader{Alg: "", KID: "my-key", Typ: "JWT"}
|
|
payload := []byte(`{"sub":"user"}`)
|
|
|
|
raw, err := signer.SignRaw(hdr, payload)
|
|
if err != nil {
|
|
t.Fatalf("SignRaw: %v", err)
|
|
}
|
|
|
|
if len(raw.Protected) == 0 {
|
|
t.Error("protected is empty")
|
|
}
|
|
if len(raw.Payload) == 0 {
|
|
t.Error("payload is empty")
|
|
}
|
|
if len(raw.Signature) == 0 {
|
|
t.Error("signature is empty")
|
|
}
|
|
|
|
// Verify the signature by round-tripping through Encode/VerifyJWT
|
|
jws, err := New(&TokenClaims{Sub: "test"})
|
|
if err != nil {
|
|
t.Fatalf("New: %v", err)
|
|
}
|
|
if err := signer.SignJWT(jws); err != nil {
|
|
t.Fatalf("SignJWT: %v", err)
|
|
}
|
|
|
|
token, err := jws.Encode()
|
|
if err != nil {
|
|
t.Fatalf("Encode: %v", err)
|
|
}
|
|
|
|
verifier := signer.Verifier()
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAllAlgorithms(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-all-algorithms")
|
|
|
|
for _, hash := range []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} {
|
|
t.Run(hash.String(), func(t *testing.T) {
|
|
signer := makeHMACSigner(t, hash, secret)
|
|
verifier := signer.Verifier()
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
jws, err := verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT: %v", err)
|
|
}
|
|
|
|
expectedAlg := signer.algorithm()
|
|
if jws.GetHeader().Alg != expectedAlg {
|
|
t.Errorf("alg = %q, want %q", jws.GetHeader().Alg, expectedAlg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHMACVerifier_AlgorithmMismatch(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
verifier := makeHMACVerifier(t, crypto.SHA384, secret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err == nil {
|
|
t.Fatal("expected error for algorithm mismatch, got nil")
|
|
}
|
|
}
|
|
|
|
func TestHMACVerifier_WrongSecret(t *testing.T) {
|
|
secret := []byte("correct-secret-long-enough-for-hs256")
|
|
wrongSecret := []byte("wrong-secret-long-enough-for-hs256")
|
|
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
verifier := makeHMACVerifier(t, crypto.SHA256, wrongSecret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong secret, got nil")
|
|
}
|
|
if !errors.Is(err, ErrSignatureInvalid) {
|
|
t.Errorf("error = %v, want %v", err, ErrSignatureInvalid)
|
|
}
|
|
}
|
|
|
|
func TestNewHMACVerifier_NilSecret(t *testing.T) {
|
|
_, err := NewHMACVerifier(crypto.SHA256, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil secret, got nil")
|
|
}
|
|
if !errors.Is(err, ErrNoVerificationKey) {
|
|
t.Errorf("error = %v, want %v", err, ErrNoVerificationKey)
|
|
}
|
|
}
|
|
|
|
func TestHMACVerifier_RetiredKeys(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
retiredSecret := []byte("retired-secret-long-enough-for-hs256")
|
|
|
|
active, err := NewSharedSecret(secret, "active")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret active: %v", err)
|
|
}
|
|
retired, err := NewSharedSecret(retiredSecret, "retired")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret retired: %v", err)
|
|
}
|
|
|
|
verifier, err := NewHMACVerifier(crypto.SHA256, active, retired)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACVerifier: %v", err)
|
|
}
|
|
|
|
// Sign with active key
|
|
signer := makeHMACSigner(t, crypto.SHA256, secret)
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
// Verify with verifier that has retired key
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT with retired: %v", err)
|
|
}
|
|
|
|
// Sign with retired key
|
|
retiredSigner := makeHMACSigner(t, crypto.SHA256, retiredSecret)
|
|
retiredToken, err := retiredSigner.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString retired: %v", err)
|
|
}
|
|
|
|
// Verify retired token with active+retired verifier
|
|
_, err = verifier.VerifyJWT(retiredToken)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT retired token: %v", err)
|
|
}
|
|
|
|
// Verify retired token with active-only verifier should fail
|
|
activeOnly, err := NewHMACVerifier(crypto.SHA256, active)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACVerifier active-only: %v", err)
|
|
}
|
|
_, err = activeOnly.VerifyJWT(retiredToken)
|
|
if err == nil {
|
|
t.Fatal("expected error for retired token with active-only verifier")
|
|
}
|
|
if !errors.Is(err, ErrSignatureInvalid) {
|
|
t.Errorf("error = %v, want %v", err, ErrSignatureInvalid)
|
|
}
|
|
}
|
|
|
|
func TestSharedSecret_Thumbprint(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough")
|
|
ss, err := NewSharedSecret(secret, "my-key")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
|
|
thumb, err := ss.Thumbprint()
|
|
if err != nil {
|
|
t.Fatalf("Thumbprint: %v", err)
|
|
}
|
|
|
|
if thumb == "" {
|
|
t.Error("thumbprint is empty")
|
|
}
|
|
|
|
// Thumbprint should not include KID
|
|
ss2, err := NewSharedSecret([]byte("test-secret-long-enough"), "different-key")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
thumb2, err := ss2.Thumbprint()
|
|
if err != nil {
|
|
t.Fatalf("Thumbprint: %v", err)
|
|
}
|
|
|
|
if thumb != thumb2 {
|
|
t.Error("thumbprints differ for same secret with different KID")
|
|
}
|
|
}
|
|
|
|
func TestSharedSecret_MarshalUnmarshal(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough")
|
|
ss, err := NewSharedSecret(secret, "my-key")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
|
|
data, err := ss.MarshalJSON()
|
|
if err != nil {
|
|
t.Fatalf("MarshalJSON: %v", err)
|
|
}
|
|
|
|
var ss2 SharedSecret
|
|
if err := ss2.UnmarshalJSON(data); err != nil {
|
|
t.Fatalf("UnmarshalJSON: %v", err)
|
|
}
|
|
|
|
if string(ss2.secret) != string(secret) {
|
|
t.Errorf("secret = %q, want %q", ss2.secret, secret)
|
|
}
|
|
|
|
if ss2.KID != "my-key" {
|
|
t.Errorf("KID = %q, want %q", ss2.KID, "my-key")
|
|
}
|
|
}
|
|
|
|
func TestSharedSecret_MarshalUnmarshal_NoKID(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough")
|
|
ss := &SharedSecret{secret: secret} // no KID
|
|
|
|
data, err := ss.MarshalJSON()
|
|
if err != nil {
|
|
t.Fatalf("MarshalJSON: %v", err)
|
|
}
|
|
|
|
var ss2 SharedSecret
|
|
if err := ss2.UnmarshalJSON(data); err != nil {
|
|
t.Fatalf("UnmarshalJSON: %v", err)
|
|
}
|
|
|
|
if ss2.KID != "" {
|
|
t.Errorf("KID = %q, want empty", ss2.KID)
|
|
}
|
|
}
|
|
|
|
func TestSharedSecret_UnmarshalJSON_EmptyK(t *testing.T) {
|
|
data := []byte(`{"kty":"oct","k":""}`)
|
|
var ss SharedSecret
|
|
err := ss.UnmarshalJSON(data)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty k, got nil")
|
|
}
|
|
if !errors.Is(err, ErrMissingKeyData) {
|
|
t.Errorf("error = %v, want %v", err, ErrMissingKeyData)
|
|
}
|
|
}
|
|
|
|
func TestParseSharedSecretJWK(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough")
|
|
ss, err := NewSharedSecret(secret, "my-key")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
|
|
data, err := ss.MarshalJSON()
|
|
if err != nil {
|
|
t.Fatalf("MarshalJSON: %v", err)
|
|
}
|
|
|
|
parsed, err := ParseSharedSecretJWK(data)
|
|
if err != nil {
|
|
t.Fatalf("ParseSharedSecretJWK: %v", err)
|
|
}
|
|
|
|
if string(parsed.secret) != string(secret) {
|
|
t.Errorf("secret = %q, want %q", parsed.secret, secret)
|
|
}
|
|
|
|
if parsed.KID != "my-key" {
|
|
t.Errorf("KID = %q, want %q", parsed.KID, "my-key")
|
|
}
|
|
}
|
|
|
|
func TestSharedSecret_KeyType(t *testing.T) {
|
|
ss, err := NewSharedSecret([]byte("test-secret-long-enough"), "")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
if ss.KeyType() != "oct" {
|
|
t.Errorf("KeyType() = %q, want %q", ss.KeyType(), "oct")
|
|
}
|
|
}
|
|
|
|
// newSharedSecret is a test helper that panics on error.
|
|
func newSharedSecret(tb testing.TB, secret []byte, kid string) *SharedSecret {
|
|
tb.Helper()
|
|
ss, err := NewSharedSecret(secret, kid)
|
|
if err != nil {
|
|
tb.Fatalf("NewSharedSecret: %v", err)
|
|
}
|
|
return ss
|
|
}
|
|
|
|
func TestNewSharedSecret_ShortSecret(t *testing.T) {
|
|
_, err := NewSharedSecret([]byte("short"), "")
|
|
if err == nil {
|
|
t.Fatal("expected error for short secret, got nil")
|
|
}
|
|
if !errors.Is(err, ErrKeyTooSmall) {
|
|
t.Errorf("error = %v, want %v", err, ErrKeyTooSmall)
|
|
}
|
|
}
|
|
|
|
func TestNewSharedSecret_EmptySecret(t *testing.T) {
|
|
_, err := NewSharedSecret([]byte{}, "")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty secret, got nil")
|
|
}
|
|
if !errors.Is(err, ErrNoSigningKey) {
|
|
t.Errorf("error = %v, want %v", err, ErrNoSigningKey)
|
|
}
|
|
}
|
|
|
|
func TestNewHMACVerifier_NilRetired(t *testing.T) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
_, err := NewHMACVerifier(crypto.SHA256, newSharedSecret(t, secret, ""), nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for nil retired secret, got nil")
|
|
}
|
|
if !errors.Is(err, ErrNoVerificationKey) {
|
|
t.Errorf("error = %v, want %v", err, ErrNoVerificationKey)
|
|
}
|
|
}
|
|
|
|
func TestHMACVerifier_KIDMatching(t *testing.T) {
|
|
secret1 := []byte("test-secret-long-enough-for-hs256-one")
|
|
secret2 := []byte("test-secret-long-enough-for-hs256-two")
|
|
|
|
key1, err := NewSharedSecret(secret1, "key-one")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret key1: %v", err)
|
|
}
|
|
key2, err := NewSharedSecret(secret2, "key-two")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret key2: %v", err)
|
|
}
|
|
|
|
verifier, err := NewHMACVerifier(crypto.SHA256, key2, key1)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACVerifier: %v", err)
|
|
}
|
|
|
|
// Sign with key1
|
|
signer1 := makeHMACSigner(t, crypto.SHA256, secret1)
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer1.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
// Verify with verifier that has key1 as retired — should succeed
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT with matching retired key: %v", err)
|
|
}
|
|
|
|
// Sign with key2
|
|
signer2 := makeHMACSigner(t, crypto.SHA256, secret2)
|
|
token2, err := signer2.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString key2: %v", err)
|
|
}
|
|
|
|
// Verify with verifier that has key2 as active — should succeed
|
|
_, err = verifier.VerifyJWT(token2)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT with matching active key: %v", err)
|
|
}
|
|
|
|
// Verify with wrong KID should fail with ErrUnknownKID
|
|
// Sign with a key that has KID set
|
|
signerWithKID, err := NewHMACSigner(crypto.SHA256, key1)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACSigner with KID: %v", err)
|
|
}
|
|
tokenWithKID, err := signerWithKID.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString with KID: %v", err)
|
|
}
|
|
|
|
verifierNoKey1, err := NewHMACVerifier(crypto.SHA256, key2)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACVerifier no-key1: %v", err)
|
|
}
|
|
_, err = verifierNoKey1.VerifyJWT(tokenWithKID)
|
|
if err == nil {
|
|
t.Fatal("expected error for KID not found, got nil")
|
|
}
|
|
if !errors.Is(err, ErrUnknownKID) {
|
|
t.Errorf("error = %v, want %v", err, ErrUnknownKID)
|
|
}
|
|
}
|
|
|
|
func TestHMACVerifier_NoKID_TriesAll(t *testing.T) {
|
|
secret1 := []byte("test-secret-long-enough-for-hs256-one")
|
|
secret2 := []byte("test-secret-long-enough-for-hs256-two")
|
|
|
|
key1, err := NewSharedSecret(secret1, "")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret key1: %v", err)
|
|
}
|
|
key2, err := NewSharedSecret(secret2, "")
|
|
if err != nil {
|
|
t.Fatalf("NewSharedSecret key2: %v", err)
|
|
}
|
|
|
|
verifier, err := NewHMACVerifier(crypto.SHA256, key2, key1)
|
|
if err != nil {
|
|
t.Fatalf("NewHMACVerifier: %v", err)
|
|
}
|
|
|
|
// Sign with key1 (no KID)
|
|
signer1 := makeHMACSigner(t, crypto.SHA256, secret1)
|
|
claims := &TokenClaims{
|
|
Iss: "test",
|
|
Sub: "user",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer1.SignToString(claims)
|
|
if err != nil {
|
|
t.Fatalf("SignToString: %v", err)
|
|
}
|
|
|
|
// Token has no KID — verifier should try all secrets
|
|
_, err = verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
t.Fatalf("VerifyJWT no-KID token: %v", err)
|
|
}
|
|
}
|
|
|
|
func BenchmarkHMACSigner_SignToString(b *testing.B) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(b, crypto.SHA256, secret)
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test-issuer",
|
|
Sub: "user-42",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
_, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkHMACVerifier_VerifyJWT(b *testing.B) {
|
|
secret := []byte("test-secret-long-enough-for-hs256")
|
|
signer := makeHMACSigner(b, crypto.SHA256, secret)
|
|
verifier := signer.Verifier()
|
|
|
|
claims := &TokenClaims{
|
|
Iss: "test-issuer",
|
|
Sub: "user-42",
|
|
Exp: time.Now().Add(time.Hour).Unix(),
|
|
}
|
|
|
|
token, err := signer.SignToString(claims)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
_, err := verifier.VerifyJWT(token)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|