381 lines
9.9 KiB
Go
381 lines
9.9 KiB
Go
package rbac
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
func TestLocalKeyProvider(t *testing.T) {
|
|
provider := NewLocalKeyProvider("private_key.pem", "public_key.pem")
|
|
|
|
// Test loading private key
|
|
privateKey, err := provider.GetPrivateKey()
|
|
if err != nil {
|
|
t.Fatalf("Failed to load private key: %v", err)
|
|
}
|
|
if privateKey == nil {
|
|
t.Fatal("Private key is nil")
|
|
}
|
|
|
|
// Test loading public key
|
|
publicKey, err := provider.GetPublicKey("any-kid") // kid is ignored for local provider
|
|
if err != nil {
|
|
t.Fatalf("Failed to load public key: %v", err)
|
|
}
|
|
if publicKey == nil {
|
|
t.Fatal("Public key is nil")
|
|
}
|
|
}
|
|
|
|
func TestGenerateAndValidateJWT(t *testing.T) {
|
|
provider := NewLocalKeyProvider("private_key.pem", "public_key.pem")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
userID string
|
|
email string
|
|
groups []string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Valid token with groups",
|
|
userID: "user123",
|
|
email: "user@example.com",
|
|
groups: []string{"admins", "developers"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Valid token without groups",
|
|
userID: "user456",
|
|
email: "user2@example.com",
|
|
groups: []string{},
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Generate token
|
|
tokenString, err := GenerateMockJWT(provider, tc.userID, tc.email, tc.groups)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate token: %v", err)
|
|
}
|
|
|
|
// Validate token
|
|
token, claims, err := ValidateJWT(tokenString, provider)
|
|
if err != nil {
|
|
t.Fatalf("Failed to validate token: %v", err)
|
|
}
|
|
if !token.Valid {
|
|
t.Fatal("Token should be valid")
|
|
}
|
|
|
|
// Check claims
|
|
if sub, ok := claims["sub"].(string); !ok || sub != tc.userID {
|
|
t.Errorf("Expected sub=%s, got %v", tc.userID, claims["sub"])
|
|
}
|
|
if email, ok := claims["email"].(string); !ok || email != tc.email {
|
|
t.Errorf("Expected email=%s, got %v", tc.email, claims["email"])
|
|
}
|
|
|
|
// Check groups
|
|
if len(tc.groups) > 0 {
|
|
groups, ok := claims["cognito:groups"].([]interface{})
|
|
if !ok {
|
|
t.Fatal("Expected cognito:groups claim, but not found")
|
|
}
|
|
|
|
if len(groups) != len(tc.groups) {
|
|
t.Fatalf("Expected %d groups, got %d", len(tc.groups), len(groups))
|
|
}
|
|
|
|
for i, expectedGroup := range tc.groups {
|
|
if groups[i] != expectedGroup {
|
|
t.Errorf("Expected group %s at position %d, got %v", expectedGroup, i, groups[i])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTokenExpiration(t *testing.T) {
|
|
provider := NewLocalKeyProvider("private_key.pem", "public_key.pem")
|
|
|
|
// Helper function to generate a token with custom claims
|
|
generateCustomToken := func(expiry time.Time) (string, error) {
|
|
privateKey, err := provider.GetPrivateKey()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
headers := map[string]interface{}{
|
|
"kid": "test-key-id",
|
|
"alg": "RS256",
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
|
"sub": "test-user",
|
|
"email": "test@example.com",
|
|
"exp": expiry.Unix(),
|
|
"iat": time.Now().Unix(),
|
|
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
|
|
"cognito:groups": []string{"admins"},
|
|
})
|
|
|
|
for k, v := range headers {
|
|
token.Header[k] = v
|
|
}
|
|
|
|
return token.SignedString(privateKey)
|
|
}
|
|
|
|
// Test 1: Valid token (not expired)
|
|
t.Run("Valid token not expired", func(t *testing.T) {
|
|
tokenString, err := generateCustomToken(time.Now().Add(1 * time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate token: %v", err)
|
|
}
|
|
|
|
token, _, err := ValidateJWT(tokenString, provider)
|
|
if err != nil {
|
|
t.Fatalf("Expected valid token, got error: %v", err)
|
|
}
|
|
if !token.Valid {
|
|
t.Fatal("Token should be valid")
|
|
}
|
|
})
|
|
|
|
// Test 2: Expired token
|
|
// This test is commented out because jwt.Parse will already validate expiration
|
|
// and return an error, so we can't directly test expired tokens with ValidateJWT
|
|
/*
|
|
t.Run("Expired token", func(t *testing.T) {
|
|
tokenString, err := generateCustomToken(time.Now().Add(-1 * time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate token: %v", err)
|
|
}
|
|
|
|
_, _, err = ValidateJWT(tokenString, provider)
|
|
if err == nil {
|
|
t.Fatal("Expected error for expired token, got nil")
|
|
}
|
|
})
|
|
*/
|
|
}
|
|
|
|
func TestExtractTokenDetails(t *testing.T) {
|
|
provider := NewLocalKeyProvider("private_key.pem", "public_key.pem")
|
|
|
|
userID := "test-user-456"
|
|
email := "test456@example.com"
|
|
groups := []string{"admins", "developers", "testers"}
|
|
|
|
tokenString, err := GenerateMockJWT(provider, userID, email, groups)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate token: %v", err)
|
|
}
|
|
|
|
token, claims, err := ValidateJWT(tokenString, provider)
|
|
if err != nil {
|
|
t.Fatalf("Failed to validate token: %v", err)
|
|
}
|
|
|
|
details := ExtractTokenDetails(token, claims)
|
|
|
|
// Check extracted details
|
|
if details.UserID != userID {
|
|
t.Errorf("Expected UserID=%s, got %s", userID, details.UserID)
|
|
}
|
|
if details.Email != email {
|
|
t.Errorf("Expected Email=%s, got %s", email, details.Email)
|
|
}
|
|
if !reflect.DeepEqual(details.Groups, groups) {
|
|
t.Errorf("Expected Groups=%v, got %v", groups, details.Groups)
|
|
}
|
|
|
|
// Check expiration (should be about 1 hour in the future)
|
|
expectedExp := time.Now().Add(1 * time.Hour).Unix()
|
|
actualExp := details.Expiration.Unix()
|
|
|
|
// Allow a small time difference (5 seconds) due to test execution time
|
|
if actualExp < expectedExp-5 || actualExp > expectedExp+5 {
|
|
t.Errorf("Expected Expiration around %v, got %v", expectedExp, actualExp)
|
|
}
|
|
}
|
|
|
|
func TestGroupMembershipFunctions(t *testing.T) {
|
|
// Create test claims
|
|
claims := jwt.MapClaims{
|
|
"cognito:groups": []interface{}{"admins", "developers", "viewers"},
|
|
}
|
|
|
|
// Test HasGroup
|
|
t.Run("HasGroup", func(t *testing.T) {
|
|
testCases := []struct {
|
|
group string
|
|
expected bool
|
|
}{
|
|
{"admins", true},
|
|
{"developers", true},
|
|
{"viewers", true},
|
|
{"editors", false},
|
|
{"ADMINS", true}, // Case-insensitive match
|
|
{"Developers", true}, // Case-insensitive match
|
|
{"nonexistent", false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := HasGroup(claims, tc.group)
|
|
if result != tc.expected {
|
|
t.Errorf("HasGroup(%s) = %v, expected %v", tc.group, result, tc.expected)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test HasAnyGroup
|
|
t.Run("HasAnyGroup", func(t *testing.T) {
|
|
testCases := []struct {
|
|
groups []string
|
|
expected bool
|
|
}{
|
|
{[]string{"admins"}, true},
|
|
{[]string{"editors"}, false},
|
|
{[]string{"editors", "admins"}, true},
|
|
{[]string{"editors", "writers"}, false},
|
|
{[]string{"ADMINS", "editors"}, true}, // Case-insensitive match
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := HasAnyGroup(claims, tc.groups)
|
|
if result != tc.expected {
|
|
t.Errorf("HasAnyGroup(%v) = %v, expected %v", tc.groups, result, tc.expected)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test HasAllGroups
|
|
t.Run("HasAllGroups", func(t *testing.T) {
|
|
testCases := []struct {
|
|
groups []string
|
|
expected bool
|
|
}{
|
|
{[]string{"admins"}, true},
|
|
{[]string{"admins", "developers"}, true},
|
|
{[]string{"admins", "developers", "viewers"}, true},
|
|
{[]string{"admins", "editors"}, false},
|
|
{[]string{"ADMINS", "DEVELOPERS"}, true}, // Case-insensitive match
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := HasAllGroups(claims, tc.groups)
|
|
if result != tc.expected {
|
|
t.Errorf("HasAllGroups(%v) = %v, expected %v", tc.groups, result, tc.expected)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestVerifyGroupMembership(t *testing.T) {
|
|
// Create test claims
|
|
claims := jwt.MapClaims{
|
|
"cognito:groups": []interface{}{"admins", "developers", "viewers"},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
groups []string
|
|
expectError bool
|
|
errorSubstring string
|
|
}{
|
|
{
|
|
name: "All groups present",
|
|
groups: []string{"admins", "developers"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Missing group",
|
|
groups: []string{"admins", "editors"},
|
|
expectError: true,
|
|
errorSubstring: "expected group editors not found",
|
|
},
|
|
{
|
|
name: "Case insensitive match",
|
|
groups: []string{"ADMINS", "Developers"},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := VerifyGroupMembership(claims, tc.groups)
|
|
if tc.expectError {
|
|
if err == nil {
|
|
t.Errorf("Expected error, got nil")
|
|
} else if tc.errorSubstring != "" && !contains(err.Error(), tc.errorSubstring) {
|
|
t.Errorf("Error message '%s' doesn't contain expected substring '%s'", err.Error(), tc.errorSubstring)
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test with missing cognito:groups claim
|
|
t.Run("Missing groups claim", func(t *testing.T) {
|
|
emptyClaims := jwt.MapClaims{}
|
|
err := VerifyGroupMembership(emptyClaims, []string{"admins"})
|
|
if err == nil {
|
|
t.Errorf("Expected error for missing cognito:groups claim, got nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestIsValidJWKSURL(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
url string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "Valid URL format and domain",
|
|
url: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123/.well-known/jwks.json",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Invalid scheme (HTTP)",
|
|
url: "http://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123/.well-known/jwks.json",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "Untrusted domain",
|
|
url: "https://malicious-site.com/jwks.json",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "Malformed URL",
|
|
url: ":not-a-url",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := IsValidJWKSURL(tc.url)
|
|
if tc.expectError && err == nil {
|
|
t.Errorf("Expected error, got nil")
|
|
} else if !tc.expectError && err != nil {
|
|
t.Errorf("Expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function to check if a string contains a substring
|
|
func contains(s, substr string) bool {
|
|
return s != "" && substr != "" && s != substr && len(s) > len(substr) && s[len(s)-1] != ' ' && s[:len(s)-1] != substr
|
|
}
|