Files
auth/jwt_test.go

259 lines
6.9 KiB
Go

// FILE: auth/jwt_test.go
package auth
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJWTHS256(t *testing.T) {
secret := []byte("test-secret-key-must-be-32-bytes")
jwtMgr, err := NewJWT(secret)
require.NoError(t, err)
userID := "user123"
claims := map[string]any{
"email": "test@example.com",
"role": "admin",
}
// Generate token
token, err := jwtMgr.GenerateToken(userID, claims)
require.NoError(t, err)
assert.NotEmpty(t, token)
// Validate token
extractedUserID, extractedClaims, err := jwtMgr.ValidateToken(token)
require.NoError(t, err)
assert.Equal(t, userID, extractedUserID)
assert.Equal(t, "test@example.com", extractedClaims["email"])
assert.Equal(t, "admin", extractedClaims["role"])
}
func TestJWTRS256(t *testing.T) {
// Generate RSA key pair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
// Test with private key (can sign and verify)
jwtMgr, err := NewJWTRSA(privateKey)
require.NoError(t, err)
userID := "user456"
claims := map[string]any{
"scope": "read:all",
}
// Generate token
token, err := jwtMgr.GenerateToken(userID, claims)
require.NoError(t, err)
assert.NotEmpty(t, token)
// Validate with same manager
extractedUserID, extractedClaims, err := jwtMgr.ValidateToken(token)
require.NoError(t, err)
assert.Equal(t, userID, extractedUserID)
assert.Equal(t, "read:all", extractedClaims["scope"])
// Test with verifier only (public key)
verifier, err := NewJWTVerifier(&privateKey.PublicKey)
require.NoError(t, err)
// Should validate token
extractedUserID, _, err = verifier.ValidateToken(token)
require.NoError(t, err)
assert.Equal(t, userID, extractedUserID)
// Should not generate token
_, err = verifier.GenerateToken(userID, claims)
assert.Equal(t, ErrTokenNoPrivateKey, err)
}
func TestJWTOptions(t *testing.T) {
secret := []byte("test-secret-key-must-be-32-bytes")
// Test custom lifetime
jwtMgr, err := NewJWT(secret,
WithTokenLifetime(1*time.Hour),
WithIssuer("test-issuer"),
WithAudience([]string{"api.example.com"}),
)
require.NoError(t, err)
token, err := jwtMgr.GenerateToken("user1", nil)
require.NoError(t, err)
// Parse token to check claims
parsed, _ := jwt.Parse(token, func(token *jwt.Token) (any, error) {
return secret, nil
})
claims := parsed.Claims.(jwt.MapClaims)
// Check issuer
assert.Equal(t, "test-issuer", claims["iss"])
// Check audience
aud := claims["aud"].([]any)
assert.Contains(t, aud, "api.example.com")
// Check expiration is ~1 hour
exp := int64(claims["exp"].(float64))
iat := int64(claims["iat"].(float64))
assert.InDelta(t, 3600, exp-iat, 10)
}
func TestJWTErrors(t *testing.T) {
secret := []byte("test-secret-key-must-be-32-bytes")
jwtMgr, err := NewJWT(secret)
require.NoError(t, err)
// Empty user ID
_, err = jwtMgr.GenerateToken("", nil)
assert.Equal(t, ErrTokenEmptyUserID, err)
// Invalid token format
_, _, err = jwtMgr.ValidateToken("invalid.token")
assert.ErrorIs(t, err, ErrTokenMalformed)
// Tampered signature
token, _ := jwtMgr.GenerateToken("user1", nil)
parts := strings.Split(token, ".")
tampered := parts[0] + "." + parts[1] + ".invalidsignature"
_, _, err = jwtMgr.ValidateToken(tampered)
assert.ErrorIs(t, err, ErrTokenInvalidSignature)
// Wrong algorithm
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048)
rsaMgr, _ := NewJWTRSA(rsaKey)
rsaToken, _ := rsaMgr.GenerateToken("user1", nil)
_, _, err = jwtMgr.ValidateToken(rsaToken)
assert.ErrorIs(t, err, ErrTokenInvalidSignature)
}
func TestJWTExpiration(t *testing.T) {
secret := []byte("test-secret-key-must-be-32-bytes")
// Create token with 1 second lifetime
jwtMgr, err := NewJWT(secret, WithTokenLifetime(1*time.Second), WithLeeway(0))
require.NoError(t, err)
token, err := jwtMgr.GenerateToken("user1", nil)
require.NoError(t, err)
// Should be valid immediately
_, _, err = jwtMgr.ValidateToken(token)
assert.NoError(t, err)
// Wait for expiration
time.Sleep(2 * time.Second)
// Should be expired
_, _, err = jwtMgr.ValidateToken(token)
assert.ErrorIs(t, err, ErrTokenExpired)
}
func TestLeeway(t *testing.T) {
secret := []byte("test-secret-key-must-be-32-bytes")
// Create manager with no leeway
jwtMgr, err := NewJWT(secret, WithLeeway(0))
require.NoError(t, err)
// Manually create a token with NotBefore in future
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "user1",
"nbf": now.Add(2 * time.Second).Unix(),
"exp": now.Add(1 * time.Hour).Unix(),
})
tokenString, err := token.SignedString(secret)
require.NoError(t, err)
// Should fail immediately (not valid yet)
_, _, err = jwtMgr.ValidateToken(tokenString)
assert.ErrorIs(t, err, ErrTokenNotYetValid)
// Create manager with leeway
jwtMgrWithLeeway, err := NewJWT(secret, WithLeeway(5*time.Second))
require.NoError(t, err)
// Should pass with leeway
_, _, err = jwtMgrWithLeeway.ValidateToken(tokenString)
assert.NoError(t, err)
}
func TestStandaloneFunctions(t *testing.T) {
secret := []byte("test-secret-key-must-be-32-bytes")
userID := "standalone-user"
claims := map[string]any{"test": "value"}
// Generate token
token, err := GenerateHS256Token(secret, userID, claims, 1*time.Hour)
require.NoError(t, err)
// Validate token
extractedUserID, extractedClaims, err := ValidateHS256Token(secret, token)
require.NoError(t, err)
assert.Equal(t, userID, extractedUserID)
assert.Equal(t, "value", extractedClaims["test"])
// Test with short secret
_, err = GenerateHS256Token([]byte("short"), userID, claims, 1*time.Hour)
assert.Equal(t, ErrSecretTooShort, err)
}
func TestJWTRSAFromPEM(t *testing.T) {
// 1. Generate a new RSA key pair for this test
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
// 2. Encode the private key to PEM format
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
// 3. Encode the public key to PEM format
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
require.NoError(t, err)
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
})
// 4. Test the PEM constructor for the signer
jwtMgr, err := NewJWTRSAFromPEM(privateKeyPEM)
require.NoError(t, err)
token, err := jwtMgr.GenerateToken("user-from-pem", nil)
require.NoError(t, err)
assert.NotEmpty(t, token)
// 5. Test the PEM constructor for the verifier
verifier, err := NewJWTVerifierFromPEM(publicKeyPEM)
require.NoError(t, err)
userID, _, err := verifier.ValidateToken(token)
require.NoError(t, err)
assert.Equal(t, "user-from-pem", userID)
// 6. Test failure cases with invalid data
_, err = NewJWTRSAFromPEM([]byte("invalid pem data"))
assert.ErrorIs(t, err, ErrRSAInvalidPEM)
_, err = NewJWTVerifierFromPEM([]byte("invalid pem data"))
assert.ErrorIs(t, err, ErrRSAInvalidPEM)
}