fix tests

add new cognito vars
This commit is contained in:
jay brown
2025-04-11 14:30:58 -07:00
parent f620118b59
commit 87d2dce06d
45 changed files with 17 additions and 4513 deletions
-135
View File
@@ -1,135 +0,0 @@
# RBAC (Role-Based Access Control) Package
This package provides authentication and authorization functionality for the application, with a specific focus on AWS Cognito integration and role-based access control (RBAC).
## Overview
The RBAC package handles all aspects of authentication and authorization, including:
- JWT token validation and verification
- AWS Cognito integration with JWKS (JSON Web Key Set) support
- Role-based access control through Cognito groups
- Echo middleware for protecting routes based on user roles
## Components
### Cognito Integration (`cognito.go`)
The package provides robust AWS Cognito integration with features including:
- JWT token validation using Cognito's JWKS endpoint
- Automatic key rotation and caching
- Group-based authorization checks
- Token details extraction and validation
Key components include:
- `CognitoKeyProvider`: Manages JWT validation keys from Cognito's JWKS endpoint
- `LocalKeyProvider`: Provides local key management for testing
- `TokenDetails`: Contains extracted JWT token information
- Various helper functions for group membership verification
## Usage
### Setting up Cognito Key Provider
```go
keyProvider := rbac.NewCognitoKeyProvider(
"us-east-1", // AWS Region
"your-user-pool-id" // Cognito User Pool ID / add to config
)
```
### Validating JWT Tokens
```go
tokenString := "your.jwt.token"
token, claims, err := rbac.ValidateJWT(tokenString, keyProvider)
if err != nil {
// Handle validation error
}
```
### Checking Group Membership
```go
// Check for a single group
if rbac.HasGroup(claims, "admin") {
// User is an admin
}
// Check for any of multiple groups
if rbac.HasAnyGroup(claims, []string{"admin", "editor"}) {
// User has at least one of the required roles
}
// Check for all required groups
if rbac.HasAllGroups(claims, []string{"editor", "reviewer"}) {
// User has all required roles
}
```
## Echo Middleware
The package includes middleware for the Echo web framework (coming soon) that will provide:
- Automatic JWT token validation
- Role-based route protection
- User context injection
### Example middleware usage
This is not tested yet.
```go
import ...
// Create the key provider once at startup
keyProvider := rbac.NewCognitoKeyProvider("us-east-1", "us-east-1_XXXXXXXX")
// Use in middleware
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString := extractTokenFromRequest(c)
_, claims, err := rbac.ValidateJWT(tokenString, keyProvider)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
}
if !rbac.HasAnyGroup(claims, []string{"admin", "editor"}) {
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}
return next(c)
}
}
```
## Security Considerations
- All JWKS endpoints must use HTTPS
- Only trusted Cognito domains are allowed
- JWT tokens are thoroughly validated including:
- Signature verification
- Expiration checking
- Required claims validation
- Group membership verification
## Testing
The package includes support for generating mock JWT tokens for testing purposes using the `LocalKeyProvider`:
```go
keyProvider := rbac.NewLocalKeyProvider(
"path/to/private.pem",
"path/to/public.pem"
)
token, err := rbac.GenerateMockJWT(
keyProvider,
"user123",
"user@example.com",
[]string{"admin", "editor"}
)
```
-407
View File
@@ -1,407 +0,0 @@
package rbac
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// KeyProvider interface for different key sources
type KeyProvider interface {
GetPublicKey(kid string) (*rsa.PublicKey, error)
}
// LocalKeyProvider implements KeyProvider using local PEM files
type LocalKeyProvider struct {
PrivateKeyPath string
PublicKeyPath string
}
// CognitoKeyProvider implements KeyProvider using AWS Cognito JWKS endpoint
type CognitoKeyProvider struct {
Region string
UserPoolID string
JwksCache map[string]*rsa.PublicKey // Cache for JWKS keys by Key ID
}
// JWKS structure for Cognito public keys
type JWKS struct {
Keys []struct {
Kid string `json:"kid"`
Kty string `json:"kty"`
N string `json:"n"`
E string `json:"e"`
Use string `json:"use"`
Alg string `json:"alg"`
} `json:"keys"`
}
// TokenDetails contains the extracted information from a JWT token
type TokenDetails struct {
Token *jwt.Token
Claims jwt.MapClaims
UserID string
Email string
Groups []string
Expiration time.Time
}
// Load private key from file
func LoadPrivateKey(keyPath string) (*rsa.PrivateKey, error) {
keyData, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("error reading private key file: %w", err)
}
return jwt.ParseRSAPrivateKeyFromPEM(keyData)
}
// Load public key from file
func LoadPublicKey(keyPath string) (*rsa.PublicKey, error) {
keyData, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("error reading public key file: %w", err)
}
return jwt.ParseRSAPublicKeyFromPEM(keyData)
}
// GetPublicKey implements KeyProvider for LocalKeyProvider
func (lkp *LocalKeyProvider) GetPublicKey(kid string) (*rsa.PublicKey, error) {
// For local provider, kid is ignored since we only have one key
return LoadPublicKey(lkp.PublicKeyPath)
}
// GetPrivateKey returns the private key for signing test tokens
func (lkp *LocalKeyProvider) GetPrivateKey() (*rsa.PrivateKey, error) {
return LoadPrivateKey(lkp.PrivateKeyPath)
}
// jwkToPublicKey converts JSON Web Key (JWK) components to an RSA public key.
// Parameters:
// - n: base64 URL-encoded string representing the modulus of the RSA key
// - e: base64 URL-encoded string representing the exponent of the RSA key
//
// Returns:
// - *rsa.PublicKey: RSA public key constructed from the provided components
// - error: if there's an issue decoding the base64 values or creating the key
func jwkToPublicKey(n, e string) (*rsa.PublicKey, error) {
// Decode the base64 URL-encoded modulus
nBytes, err := base64.RawURLEncoding.DecodeString(n)
if err != nil {
return nil, fmt.Errorf("error decoding modulus: %w", err)
}
// Decode the base64 URL-encoded exponent
eBytes, err := base64.RawURLEncoding.DecodeString(e)
if err != nil {
return nil, fmt.Errorf("error decoding exponent: %w", err)
}
// Convert modulus bytes to big.Int
modulus := new(big.Int)
modulus.SetBytes(nBytes)
// Convert exponent bytes to int
var exponent int
for i := 0; i < len(eBytes); i++ {
exponent = exponent<<8 + int(eBytes[i])
}
// Create RSA public key
return &rsa.PublicKey{
N: modulus,
E: exponent,
}, nil
}
// GetPublicKey implements KeyProvider for CognitoKeyProvider
func (ckp *CognitoKeyProvider) GetPublicKey(kid string) (*rsa.PublicKey, error) {
// Check cache first
if ckp.JwksCache == nil {
ckp.JwksCache = make(map[string]*rsa.PublicKey)
}
if key, exists := ckp.JwksCache[kid]; exists {
return key, nil
}
// Fetch JWKS from Cognito (old way)
// baseURL := "https://cognito-idp.amazonaws.com"
// jwksPath := fmt.Sprintf("/%s/%s/.well-known/jwks.json", ckp.AuthRegion, ckp.AuthUserPoolID)
// https://cognito-idp.us-east-2.amazonaws.com/us-east-2_1y6po8rR8/.well-known/jwks.json
jwksURL := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", ckp.Region, ckp.UserPoolID)
if err := IsValidJWKSURL(jwksURL); err != nil {
return nil, fmt.Errorf("invalid JWKS URL: %w", err)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("GET", jwksURL, nil)
if err != nil {
return nil, fmt.Errorf("error creating request for JWKS: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetching JWKS from Cognito: %w", err)
}
defer resp.Body.Close()
jwksData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading JWKS response: %w", err)
}
var jwks JWKS
if err := json.Unmarshal(jwksData, &jwks); err != nil {
return nil, fmt.Errorf("error parsing JWKS JSON: %w", err)
}
// Find the key with matching kid
for _, key := range jwks.Keys {
if key.Kid == kid {
// Convert JWK to RSA public key
publicKey, err := jwkToPublicKey(key.N, key.E)
if err != nil {
return nil, fmt.Errorf("error converting JWK to public key: %w", err)
}
// Store in cache
ckp.JwksCache[kid] = publicKey
return publicKey, nil
}
}
return nil, fmt.Errorf("no matching key found with kid: %s", kid)
}
// NewLocalKeyProvider creates a new LocalKeyProvider
func NewLocalKeyProvider(privateKeyPath, publicKeyPath string) *LocalKeyProvider {
return &LocalKeyProvider{
PrivateKeyPath: privateKeyPath,
PublicKeyPath: publicKeyPath,
}
}
// NewCognitoKeyProvider creates a new CognitoKeyProvider
func NewCognitoKeyProvider(region, userPoolID string) *CognitoKeyProvider {
return &CognitoKeyProvider{
Region: region,
UserPoolID: userPoolID,
JwksCache: make(map[string]*rsa.PublicKey),
}
}
// GenerateTestJWT creates a test JWT with given groups
func GenerateTestJWT(keyProvider *LocalKeyProvider, userID, email string, groups []string) (string, error) {
privateKey, err := keyProvider.GetPrivateKey()
if err != nil {
return "", err
}
// Create a header with kid value (important for verification)
headers := map[string]interface{}{
"kid": "test-key-id", // In a real scenario this would match the key ID from Cognito
"alg": "RS256",
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"sub": userID,
"email": email,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX",
"cognito:groups": groups,
})
// Set the header
for k, v := range headers {
token.Header[k] = v
}
return token.SignedString(privateKey)
}
// PrintTokenDetails prints the JWT token details for debugging
func PrintTokenDetails(token *jwt.Token) {
fmt.Println("\n=== JWT Token Details ===")
// Print Header
headerJSON, _ := json.MarshalIndent(token.Header, "", " ")
fmt.Printf("\nHeader:\n%s\n", string(headerJSON))
// Print Claims
claimsJSON, _ := json.MarshalIndent(token.Claims, "", " ")
fmt.Printf("\nClaims:\n%s\n", string(claimsJSON))
// Print Signature
fmt.Printf("\nSignature: %s\n", token.Signature)
// Print Method
fmt.Printf("\nSigning Method: %s\n", token.Method.Alg())
// Print Valid status
fmt.Printf("\nValid: %v\n", token.Valid)
fmt.Println("\n=====================")
}
// ValidateJWT validates a JWT token and returns the claims
func ValidateJWT(tokenString string, keyProvider KeyProvider) (*jwt.Token, jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, errors.New("unexpected signing method")
}
// Get key ID from token header
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("no key ID (kid) in token header")
}
// Get public key from provider
return keyProvider.GetPublicKey(kid)
})
if err != nil {
return nil, nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return token, claims, nil
}
return nil, nil, errors.New("invalid token")
}
// ExtractTokenDetails extracts and organizes token information
func ExtractTokenDetails(token *jwt.Token, claims jwt.MapClaims) *TokenDetails {
details := &TokenDetails{
Token: token,
Claims: claims,
}
if sub, ok := claims["sub"].(string); ok {
details.UserID = sub
}
if email, ok := claims["email"].(string); ok {
details.Email = email
}
if expFloat, ok := claims["exp"].(float64); ok {
details.Expiration = time.Unix(int64(expFloat), 0)
}
if groupsInterface, ok := claims["cognito:groups"].([]interface{}); ok {
details.Groups = make([]string, 0, len(groupsInterface))
for _, group := range groupsInterface {
if groupStr, ok := group.(string); ok {
details.Groups = append(details.Groups, groupStr)
}
}
}
return details
}
// HasGroup checks if a user belongs to a specific group
func HasGroup(claims jwt.MapClaims, requiredGroup string) bool {
if groups, ok := claims["cognito:groups"].([]interface{}); ok {
for _, group := range groups {
if groupStr, ok := group.(string); ok && strings.EqualFold(groupStr, requiredGroup) {
return true
}
}
}
return false
}
// HasAnyGroup checks if a user belongs to any of the specified groups
func HasAnyGroup(claims jwt.MapClaims, requiredGroups []string) bool {
for _, group := range requiredGroups {
if HasGroup(claims, group) {
return true
}
}
return false
}
// HasAllGroups checks if a user belongs to all of the specified groups
func HasAllGroups(claims jwt.MapClaims, requiredGroups []string) bool {
for _, group := range requiredGroups {
if !HasGroup(claims, group) {
return false
}
}
return true
}
// IsValidJWKSURL validates that a JWKS URL is secure and from a trusted domain
func IsValidJWKSURL(urlStr string) error {
parsed, err := url.Parse(urlStr)
if err != nil {
return fmt.Errorf("invalid URL format: %w", err)
}
// Add your trusted domains here
trustedDomains := []string{
"cognito-idp.us-east-1.amazonaws.com",
"cognito-idp.us-west-2.amazonaws.com",
// Add other AWS Cognito regions as needed
}
isAllowed := false
for _, domain := range trustedDomains {
if parsed.Host == domain {
isAllowed = true
break
}
}
if !isAllowed {
return fmt.Errorf("untrusted JWKS domain: %s", parsed.Host)
}
// Ensure HTTPS
if parsed.Scheme != "https" {
return fmt.Errorf("JWKS URL must use HTTPS")
}
return nil
}
// VerifyGroupMembership verifies a user belongs to specified groups
func VerifyGroupMembership(claims jwt.MapClaims, expectedGroups []string) error {
groups, ok := claims["cognito:groups"].([]interface{})
if !ok {
return fmt.Errorf("expected 'cognito:groups' claim, but not found")
}
for _, eg := range expectedGroups {
found := false
for _, g := range groups {
if gStr, ok := g.(string); ok && strings.EqualFold(gStr, eg) {
found = true
break
}
}
if !found {
return fmt.Errorf("expected group %s not found in token", eg)
}
}
return nil
}
-590
View File
@@ -1,590 +0,0 @@
package rbac_test
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"queryorchestration/internal/rbac"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
)
// TestLocalKeyProvider validates that the LocalKeyProvider can successfully
// load both private and public keys from the specified PEM files.
func TestLocalKeyProvider(t *testing.T) {
provider := rbac.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")
}
}
// TestGenerateAndValidateJWT tests the complete JWT lifecycle:
// 1. Generating a mock JWT token with specific claims (userID, email, groups)
// 2. Validating the token with the same key provider
// 3. Verifying that all claims are correctly preserved and can be retrieved
// This test covers both tokens with and without group information.
func TestGenerateAndValidateJWT(t *testing.T) {
provider := rbac.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 := rbac.GenerateTestJWT(provider, tc.userID, tc.email, tc.groups)
if err != nil {
t.Fatalf("Failed to generate token: %v", err)
}
// Validate token
token, claims, err := rbac.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])
}
}
}
})
}
}
// TestTokenExpiration verifies that token expiration works correctly:
// 1. A token with a future expiration time should be considered valid
// 2. A token with a past expiration time would be rejected by the JWT library
// The actual expired token test is commented out because jwt.Parse
// automatically validates expiration times.
func TestTokenExpiration(t *testing.T) {
provider := rbac.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 := rbac.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 = rbac.ValidateJWT(tokenString, provider)
if err == nil {
t.Fatal("Expected error for expired token, got nil")
}
})
*/
}
// TestExtractTokenDetails verifies that the ExtractTokenDetails function
// correctly extracts and organizes user information from the JWT token:
// - UserID
// - Email
// - Group memberships
// - Token expiration time
// This ensures user context can be properly reconstructed from the token.
func TestExtractTokenDetails(t *testing.T) {
provider := rbac.NewLocalKeyProvider("private_key.pem", "public_key.pem")
userID := "test-user-456"
email := "test456@example.com"
groups := []string{"admins", "developers", "testers"}
tokenString, err := rbac.GenerateTestJWT(provider, userID, email, groups)
if err != nil {
t.Fatalf("Failed to generate token: %v", err)
}
token, claims, err := rbac.ValidateJWT(tokenString, provider)
if err != nil {
t.Fatalf("Failed to validate token: %v", err)
}
details := rbac.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)
}
}
// TestGroupMembershipFunctions validates the three group membership check functions:
// 1. HasGroup - Tests if a user belongs to a specific group
// 2. HasAnyGroup - Tests if a user belongs to at least one of several groups
// 3. HasAllGroups - Tests if a user belongs to all specified groups
// Each test includes positive and negative cases, as well as case-insensitive matching.
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
}
for _, tc := range testCases {
t.Run(tc.group, func(t *testing.T) {
result := rbac.HasGroup(claims, tc.group)
if result != tc.expected {
t.Errorf("HasGroup(%q) = %v; want %v", tc.group, result, tc.expected)
}
})
}
})
// Test HasAnyGroup
t.Run("HasAnyGroup", func(t *testing.T) {
testCases := []struct {
groups []string
expected bool
}{
{[]string{"admins", "editors"}, true},
{[]string{"editors", "writers"}, false},
{[]string{"ADMINS", "editors"}, true}, // Case-insensitive match
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%v", tc.groups), func(t *testing.T) {
result := rbac.HasAnyGroup(claims, tc.groups)
if result != tc.expected {
t.Errorf("HasAnyGroup(%v) = %v; want %v", tc.groups, result, tc.expected)
}
})
}
})
// Test HasAllGroups
t.Run("HasAllGroups", func(t *testing.T) {
testCases := []struct {
groups []string
expected bool
}{
{[]string{"admins", "developers"}, true},
{[]string{"admins", "editors"}, false},
{[]string{"ADMINS", "DEVELOPERS"}, true}, // Case-insensitive match
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%v", tc.groups), func(t *testing.T) {
result := rbac.HasAllGroups(claims, tc.groups)
if result != tc.expected {
t.Errorf("HasAllGroups(%v) = %v; want %v", tc.groups, result, tc.expected)
}
})
}
})
}
// TestVerifyGroupMembership validates the VerifyGroupMembership function,
// which ensures that a user belongs to all required groups.
// It tests:
// 1. Successful case where all required groups are present
// 2. Error case where a required group is missing
// 3. Case-insensitive group matching
// 4. Error handling when the cognito:groups claim is missing entirely
func TestVerifyGroupMembership(t *testing.T) {
claims := jwt.MapClaims{
"cognito:groups": []interface{}{"admins", "developers", "viewers"},
}
testCases := []struct {
name string
expectedGroups []string
wantErr bool
}{
{
name: "All groups present",
expectedGroups: []string{"admins", "developers"},
wantErr: false,
},
{
name: "Missing group",
expectedGroups: []string{"admins", "editors"},
wantErr: true,
},
{
name: "Case insensitive match",
expectedGroups: []string{"ADMINS", "DEVELOPERS"},
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := rbac.VerifyGroupMembership(claims, tc.expectedGroups)
if tc.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
// TestIsValidJWKSURL tests the URL validation for JWKS endpoints.
//
// JWKS (JSON Web Key Set) is a standard format used to publish public keys as a set of JWKs (JSON Web Keys).
// In the AWS Cognito context, JWKS endpoints allow services to retrieve the public keys needed to verify
// JWT tokens issued by Cognito. These endpoints must be secured (HTTPS) and trusted to prevent
// token forgery attacks.
//
// This test ensures that:
// 1. Valid Cognito JWKS URLs are accepted
// 2. Insecure URLs (HTTP instead of HTTPS) are rejected
// 3. URLs from untrusted domains are rejected
// 4. Malformed URLs are rejected
//
// This validation is critical for preventing security issues like malicious redirects,
// SSRF (Server-Side Request Forgery) attacks, or using untrusted key sources for JWT verification.
// This is important for preventing malicious redirects or SSRF attacks.
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 := rbac.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)
}
})
}
}
// Additional tests added 3/19/25 follow.
////////////////////////////////////////////////////////////////////////////////
// This test uses reflection to access the unexported jwkToPublicKey function
func TestJwkToPublicKey(t *testing.T) {
// Example valid modulus (n) and exponent (e) encoded in base64 URL format
n := "ANmflPLn7S9tj88H-HiJ5mHaQ-4F9JhOZ9EHpQGC_iJ1aXm9tZso4YF9Qf1S5Pa1vX7JqJhudcAnvQIRkcwE0xj8GfJQm0bgWDulnGnQZBM-MQXs-9ZCh24z0kDGeWL3osvAew8pyzvP68y5tie2QHsucbj35Y7_5aiTn0DmvXYRpbQxLJ70xvi7Zp7wGi5rFBThjNrO-c-jIaAnZzupfG1LNz5Bpgpc"
e := "AQAB"
// Manual implementation of jwkToPublicKey for validation
nBytes, err := base64.RawURLEncoding.DecodeString(n)
assert.NoError(t, err)
eBytes, err := base64.RawURLEncoding.DecodeString(e)
assert.NoError(t, err)
modulus := new(big.Int)
modulus.SetBytes(nBytes)
var exponent int
for i := 0; i < len(eBytes); i++ {
exponent = exponent<<8 + int(eBytes[i])
}
// Now set up a mock server to test the GetPublicKey method
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwksJSON := fmt.Sprintf(`{
"keys": [
{
"kid": "test-key-id",
"kty": "RSA",
"n": "%s",
"e": "%s",
"use": "sig",
"alg": "RS256"
}
]
}`, n, e)
w.Header().Set("Content-Type", "application/json")
_, err2 := w.Write([]byte(jwksJSON))
if err2 != nil {
return
}
}))
defer server.Close()
// Create a test JWKS result for validation
var testJWKS rbac.JWKS
testJWKSData := fmt.Sprintf(`{
"keys": [
{
"kid": "test-key-id",
"kty": "RSA",
"n": "%s",
"e": "%s",
"use": "sig",
"alg": "RS256"
}
]
}`, n, e)
err = json.Unmarshal([]byte(testJWKSData), &testJWKS)
assert.NoError(t, err)
// The test itself just verifies that we can parse the JWKS data
// This indirectly tests jwkToPublicKey since it's called inside GetPublicKey
assert.Equal(t, "test-key-id", testJWKS.Keys[0].Kid)
assert.Equal(t, n, testJWKS.Keys[0].N)
assert.Equal(t, e, testJWKS.Keys[0].E)
}
// This test implements a mock HTTP server to test CognitoKeyProvider.GetPublicKey
func TestCognitoKeyProviderGetPublicKey(t *testing.T) {
// Set up a mock HTTP server to simulate the JWKS endpoint
n := "ANmflPLn7S9tj88H-HiJ5mHaQ-4F9JhOZ9EHpQGC_iJ1aXm9tZso4YF9Qf1S5Pa1vX7JqJhudcAnvQIRkcwE0xj8GfJQm0bgWDulnGnQZBM-MQXs-9ZCh24z0kDGeWL3osvAew8pyzvP68y5tie2QHsucbj35Y7_5aiTn0DmvXYRpbQxLJ70xvi7Zp7wGi5rFBThjNrO-c-jIaAnZzupfG1LNz5Bpgpc"
e := "AQAB"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Make sure the request path matches what we expect
assert.Contains(t, r.URL.Path, "/.well-known/jwks.json")
jwksJSON := fmt.Sprintf(`{
"keys": [
{
"kid": "test-key-id",
"kty": "RSA",
"n": "%s",
"e": "%s",
"use": "sig",
"alg": "RS256"
}
]
}`, n, e)
w.Header().Set("Content-Type", "application/json")
_, err := w.Write([]byte(jwksJSON))
if err != nil {
return
}
}))
defer server.Close()
// Override the host and protocol in the CognitoKeyProvider
// We'll create a standard CognitoKeyProvider and then use reflection to modify its fields
provider := rbac.NewCognitoKeyProvider("us-east-1", "test-pool-id")
// Use reflection to modify the actual URL used by GetPublicKey
// This isn't the cleanest approach, but it allows us to test without modifying the original code
providerValue := reflect.ValueOf(provider)
// Get the cache field to check caching
jwksCacheField := reflect.Indirect(providerValue).FieldByName("JwksCache")
//jwksCache := jwksCacheField.Interface().(map[string]*rsa.PublicKey)
//assert.NotNil(t, jwksCache)
jwksCacheInterface := jwksCacheField.Interface()
jwksCache, ok := jwksCacheInterface.(map[string]*rsa.PublicKey)
assert.True(t, ok, "JwksCache field is not of expected type")
assert.NotNil(t, jwksCache)
// Instead of trying to patch the URL (which is difficult with reflection),
// we'll verify that the provider was created correctly
regionField := reflect.Indirect(providerValue).FieldByName("AuthRegion")
userPoolIDField := reflect.Indirect(providerValue).FieldByName("AuthUserPoolID")
assert.Equal(t, "us-east-1", regionField.String())
assert.Equal(t, "test-pool-id", userPoolIDField.String())
}
// TestNewCognitoKeyProvider tests the NewCognitoKeyProvider function
func TestNewCognitoKeyProvider(t *testing.T) {
// Create a provider with test values
region := "us-west-2"
userPoolID := "us-west-2_testpool"
provider := rbac.NewCognitoKeyProvider(region, userPoolID)
assert.NotNil(t, provider)
// Use reflection to check the internal fields
providerValue := reflect.ValueOf(provider).Elem()
regionField := providerValue.FieldByName("AuthRegion")
assert.Equal(t, region, regionField.String())
userPoolIDField := providerValue.FieldByName("AuthUserPoolID")
assert.Equal(t, userPoolID, userPoolIDField.String())
jwksCacheField := providerValue.FieldByName("JwksCache")
jwksCacheInterface := jwksCacheField.Interface()
jwksCache, ok := jwksCacheInterface.(map[string]*rsa.PublicKey)
assert.True(t, ok, "JwksCache field is not of expected type")
assert.NotNil(t, jwksCache)
assert.Len(t, jwksCache, 0) // Should be empty initially
}
// TestPrintTokenDetails tests the PrintTokenDetails function
func TestPrintTokenDetails(t *testing.T) {
// Create a simple JWT token
token := &jwt.Token{
Header: map[string]interface{}{
"alg": "RS256",
"kid": "test-key-id",
},
Claims: jwt.MapClaims{
"sub": "user123",
"email": "user@example.com",
"exp": float64(time.Now().Add(time.Hour).Unix()),
},
Method: jwt.SigningMethodRS256,
Signature: []byte("sample-signature"),
Valid: true,
}
// Since PrintTokenDetails just prints to stdout and doesn't return anything,
// we'll just call it and make sure it doesn't panic for coverage.
rbac.PrintTokenDetails(token)
}
-261
View File
@@ -1,261 +0,0 @@
package rbac
import (
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// JWTConfig contains configuration for the JWT middleware
type JWTConfig struct {
KeyProvider KeyProvider
TokenLookup string // Format: "header:<name>" or "query:<name>" or "cookie:<name>"
AuthScheme string // Usually "Bearer"
RequiredGroups []string // Groups that are required for access (any one of these)
ContextKey string // Key to store the claims in the context
ErrorHandler func(echo.Context, error) error
Logger *slog.Logger // Logger instance for middleware
}
// DefaultJWTConfig is the default JWT auth middleware config
var DefaultJWTConfig = JWTConfig{
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
ContextKey: "user",
ErrorHandler: defaultErrorHandler,
}
// defaultErrorHandler returns a 401 Unauthorized error for JWT validation failures
func defaultErrorHandler(c echo.Context, err error) error {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt")
}
// extractToken determines which token extractor to use based on config
func extractToken(config JWTConfig) func(echo.Context) (string, error) {
parts := strings.Split(config.TokenLookup, ":")
extractor := tokenFromHeader(parts[1], config.AuthScheme)
if parts[0] == "query" {
extractor = tokenFromQuery(parts[1])
} else if parts[0] == "cookie" {
extractor = tokenFromCookie(parts[1])
}
return extractor
}
// validateGroups checks if the user belongs to any of the required groups
func validateGroups(claims jwt.MapClaims, requiredGroups []string, logger *slog.Logger) bool {
if len(requiredGroups) == 0 {
logger.Debug("no required groups specified, access granted")
return true
}
groups, ok := claims["cognito:groups"].([]interface{})
if !ok {
logger.Warn("no groups found in claims", "user", claims["sub"])
return false
}
userGroups := make([]string, 0)
for _, group := range groups {
if groupStr, ok := group.(string); ok {
userGroups = append(userGroups, groupStr)
}
}
for _, reqGroup := range requiredGroups {
for _, group := range groups {
if groupStr, ok := group.(string); ok && strings.EqualFold(groupStr, reqGroup) {
logger.Debug("group validation successful",
"user", claims["sub"],
"required_group", reqGroup,
"user_groups", userGroups)
return true
}
}
}
logger.Warn("group validation failed",
"user", claims["sub"],
"required_groups", requiredGroups,
"user_groups", userGroups)
return false
}
// parseJWT parses and validates the JWT token
func parseJWT(tokenString string, keyProvider KeyProvider, logger *slog.Logger) (*jwt.Token, jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
logger.Error("unexpected signing method", "method", token.Method.Alg())
return nil, errors.New("unexpected signing method")
}
// Get key ID from token header
kid, ok := token.Header["kid"].(string)
if !ok {
logger.Error("no key ID in token header")
return nil, errors.New("no key ID (kid) in token header")
}
// Get public key from provider
return keyProvider.GetPublicKey(kid)
})
if err != nil {
logger.Error("failed to parse JWT", "error", err)
return nil, nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
logger.Error("invalid token claims")
return nil, nil, errors.New("invalid token")
}
logger.Debug("JWT parsed successfully",
"user", claims["sub"],
"email", claims["email"])
return token, claims, nil
}
// JWTWithConfig returns a JWT auth middleware with config
func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
// Set defaults
if config.TokenLookup == "" {
config.TokenLookup = DefaultJWTConfig.TokenLookup
}
if config.AuthScheme == "" {
config.AuthScheme = DefaultJWTConfig.AuthScheme
}
if config.ContextKey == "" {
config.ContextKey = DefaultJWTConfig.ContextKey
}
if config.ErrorHandler == nil {
config.ErrorHandler = DefaultJWTConfig.ErrorHandler
}
if config.KeyProvider == nil {
panic("JWT middleware requires a KeyProvider")
}
if config.Logger == nil {
config.Logger = slog.Default()
}
extractor := extractToken(config)
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString, err := extractor(c)
if err != nil {
config.Logger.Error("failed to extract token",
"error", err,
"path", c.Request().URL.Path,
"method", c.Request().Method)
return config.ErrorHandler(c, err)
}
_, claims, err := parseJWT(tokenString, config.KeyProvider, config.Logger)
if err != nil {
config.Logger.Error("failed to validate JWT",
"error", err,
"path", c.Request().URL.Path,
"method", c.Request().Method)
return config.ErrorHandler(c, err)
}
// Check required groups if specified
if !validateGroups(claims, config.RequiredGroups, config.Logger) {
config.Logger.Warn("insufficient permissions",
"user", claims["sub"],
"path", c.Request().URL.Path,
"method", c.Request().Method)
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}
// Store user information in context
c.Set(config.ContextKey, claims)
config.Logger.Info("authenticated request",
"user", claims["sub"],
"path", c.Request().URL.Path,
"method", c.Request().Method)
return next(c)
}
}
}
// JWT returns a JWT auth middleware with default configuration
func JWT(keyProvider KeyProvider) echo.MiddlewareFunc {
config := DefaultJWTConfig
config.KeyProvider = keyProvider
return JWTWithConfig(config)
}
// tokenFromHeader extracts token from Authorization header
func tokenFromHeader(header string, authScheme string) func(echo.Context) (string, error) {
return func(c echo.Context) (string, error) {
auth := c.Request().Header.Get(header)
if auth == "" {
return "", fmt.Errorf("missing auth header: %s", header)
}
l := len(authScheme)
if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil
}
return "", fmt.Errorf("invalid auth header format for scheme: %s", authScheme)
}
}
// tokenFromQuery extracts token from query parameter
func tokenFromQuery(param string) func(echo.Context) (string, error) {
return func(c echo.Context) (string, error) {
token := c.QueryParam(param)
if token == "" {
return "", errors.New("missing auth query parameter")
}
return token, nil
}
}
// tokenFromCookie extracts token from cookie
func tokenFromCookie(name string) func(echo.Context) (string, error) {
return func(c echo.Context) (string, error) {
cookie, err := c.Cookie(name)
if err != nil {
return "", err
}
return cookie.Value, nil
}
}
//// GroupGuard middleware ensures a user has at least one of the required groups
//func GroupGuard(requiredGroups ...string) echo.MiddlewareFunc {
// return func(next echo.HandlerFunc) echo.HandlerFunc {
// return func(c echo.Context) error {
// userClaims, ok := c.Get("user").(jwt.MapClaims)
// if !ok {
// return echo.NewHTTPError(http.StatusUnauthorized, "user not authenticated")
// }
//
// groups, ok := userClaims["cognito:groups"].([]interface{})
// if !ok {
// return echo.NewHTTPError(http.StatusForbidden, "no groups found for user")
// }
//
// for _, requiredGroup := range requiredGroups {
// for _, group := range groups {
// if groupStr, ok := group.(string); ok && strings.EqualFold(groupStr, requiredGroup) {
// return next(c)
// }
// }
// }
//
// return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
// }
// }
//}
-228
View File
@@ -1,228 +0,0 @@
package rbac_test
import (
"net/http"
"net/http/httptest"
"testing"
"queryorchestration/internal/rbac"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
// testSetup creates a new Echo instance and key provider for testing
func testSetup(t *testing.T) (*echo.Echo, *rbac.LocalKeyProvider) {
// Use local key files
privateKeyPath := "./private_key.pem"
publicKeyPath := "./public_key.pem"
// Create and initialize key provider
keyProvider := rbac.NewLocalKeyProvider(privateKeyPath, publicKeyPath)
//TestKeyProvider(t, privateKeyPath, publicKeyPath)
// Initialize Echo instance
e := echo.New()
return e, keyProvider
}
// createTestEndpoint creates a test endpoint with the given JWT config
func createTestEndpoint(e *echo.Echo, config rbac.JWTConfig) {
e.GET("/test", func(c echo.Context) error {
return c.String(http.StatusOK, "authorized")
}, rbac.JWTWithConfig(config))
}
// makeTestRequest creates and executes a test request with the given token
func makeTestRequest(t *testing.T, e *echo.Echo, token string, setupReq func(*http.Request)) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
if setupReq != nil {
setupReq(req)
} else {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
return rec
}
// TestJWTMiddleware tests the core JWT middleware functionality with group-based authorization.
// It validates:
// 1. A user with the required 'foo' group can access a protected endpoint
// 2. A user with only 'bar' group (and not 'foo') is properly denied access
// 3. A user with multiple groups including the required 'foo' group is granted access
// This test demonstrates the basic setup for protecting routes with the JWT middleware
// and ensuring only users with appropriate group membership can access restricted resources.
func TestJWTMiddleware(t *testing.T) {
e, keyProvider := testSetup(t)
// Set up a restricted endpoint that requires the 'foo' group
createTestEndpoint(e, rbac.JWTConfig{
KeyProvider: keyProvider,
RequiredGroups: []string{"foo"},
})
// Test cases
tests := []struct {
name string
groups []string
wantStatus int
}{
{
name: "Authorized user with 'foo' group",
groups: []string{"foo"},
wantStatus: http.StatusOK,
},
{
name: "Unauthorized user with 'bar' group",
groups: []string{"bar"},
wantStatus: http.StatusForbidden,
},
{
name: "User with multiple groups including required",
groups: []string{"bar", "foo", "baz"},
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate JWT token with test groups
token, err := rbac.GenerateTestJWT(keyProvider, "test-user-id", "test@example.com", tt.groups)
if err != nil {
t.Fatalf("Failed to generate JWT: %v", err)
}
// Make test request and verify response
rec := makeTestRequest(t, e, token, nil)
assert.Equal(t, tt.wantStatus, rec.Code)
if tt.wantStatus == http.StatusOK {
assert.Equal(t, "authorized", rec.Body.String())
}
})
}
}
// TestTokenExtraction validates different methods for extracting JWT tokens:
// 1. From Authorization header (standard Bearer token)
// 2. From URL query parameters
// 3. From cookies
// Each method is configured through the TokenLookup field in JWTConfig.
// The test confirms that the middleware can successfully authenticate a user
// regardless of where the token is provided, as long as it's valid and
// contains the required group membership.
func TestTokenExtraction(t *testing.T) {
_, keyProvider := testSetup(t)
// Generate JWT token with 'foo' group
token, err := rbac.GenerateTestJWT(keyProvider, "test-user-id", "test@example.com", []string{"foo"})
if err != nil {
t.Fatalf("Failed to generate JWT: %v", err)
}
// Test different token extraction methods
tests := []struct {
name string
config rbac.JWTConfig
setupReq func(req *http.Request)
wantStatus int
}{
{
name: "Extract from header",
config: rbac.JWTConfig{
KeyProvider: keyProvider,
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
RequiredGroups: []string{"foo"},
},
setupReq: func(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+token)
},
wantStatus: http.StatusOK,
},
{
name: "Extract from query parameter",
config: rbac.JWTConfig{
KeyProvider: keyProvider,
TokenLookup: "query:token",
RequiredGroups: []string{"foo"},
},
setupReq: func(req *http.Request) {
q := req.URL.Query()
q.Add("token", token)
req.URL.RawQuery = q.Encode()
},
wantStatus: http.StatusOK,
},
{
name: "Extract from cookie",
config: rbac.JWTConfig{
KeyProvider: keyProvider,
TokenLookup: "cookie:jwt",
RequiredGroups: []string{"foo"},
},
setupReq: func(req *http.Request) {
req.AddCookie(&http.Cookie{
Name: "jwt",
Value: token,
})
},
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create new Echo instance for each test to avoid config conflicts
e := echo.New()
createTestEndpoint(e, tt.config)
// Make test request and verify response
rec := makeTestRequest(t, e, token, tt.setupReq)
assert.Equal(t, tt.wantStatus, rec.Code)
if tt.wantStatus == http.StatusOK {
assert.Equal(t, "authorized", rec.Body.String())
}
})
}
}
// TestCustomErrorHandler verifies that custom error handling works correctly.
// It tests:
// 1. Setting up a custom error handler function in the JWTConfig
// 2. Sending an invalid JWT token in the request
// 3. Confirming that the custom error handler is invoked and provides
// the expected response format and status code
//
// This allows for customized error responses when authentication fails,
// which is useful for providing more context or formatting errors
// according to API standards.
func TestCustomErrorHandler(t *testing.T) {
e, keyProvider := testSetup(t)
// Custom error handler
customErrorHandler := func(c echo.Context, err error) error {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Custom auth error: " + err.Error(),
})
}
// Set up a restricted endpoint with custom error handler
createTestEndpoint(e, rbac.JWTConfig{
KeyProvider: keyProvider,
RequiredGroups: []string{"foo"},
ErrorHandler: customErrorHandler,
})
// Create test request with invalid token and verify response
rec := makeTestRequest(t, e, "invalidtoken", nil)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.Contains(t, rec.Body.String(), "Custom auth error")
}
// These tests require valid RSA key files (private_key.pem and public_key.pem)
// in the current directory. These keys are used to sign and verify JWT tokens
// during the tests. In a production environment, the public key would typically
// be retrieved from AWS Cognito's JWKS endpoint, but for testing, we use local files.
// Make sure to generate these key files before running the tests.
-128
View File
@@ -1,128 +0,0 @@
package rbac_test
import (
"crypto/rsa"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"queryorchestration/internal/rbac"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
// mockKeyProvider is a mock implementation of the KeyProvider interface
type mockKeyProvider struct{}
// GetPublicKey returns a mock public key
func (m *mockKeyProvider) GetPublicKey(kid string) (*rsa.PublicKey, error) {
// Create a mock RSA public key with minimal valid structure
return &rsa.PublicKey{
N: big.NewInt(123), // Just a dummy value
E: 65537, // Common RSA exponent
}, nil
}
// createMockKeyProvider creates a mock KeyProvider for testing
func createMockKeyProvider() rbac.KeyProvider {
return &mockKeyProvider{}
}
// TestDefaultErrorHandler tests the defaultErrorHandler function
func TestDefaultErrorHandler(t *testing.T) {
// Create a new Echo instance
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Create a test error
//testErr := errors.New("test error")
// Access the defaultErrorHandler function directly
// In a real implementation, you might use an exported test helper function like:
// err := rbac.DefaultErrorHandlerTest(c, testErr)
// In this case, we'll test the error handler through the Echo middleware
// by creating a config with our own error handler that logs what was called
var capturedError error
customErrorHandler := func(c echo.Context, err error) error {
capturedError = err
return echo.NewHTTPError(http.StatusUnauthorized, "captured: "+err.Error())
}
config := rbac.JWTConfig{
KeyProvider: createMockKeyProvider(),
ErrorHandler: customErrorHandler,
}
// Create middleware with our config
middleware := rbac.JWTWithConfig(config)
// Create a handler that should never be called
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "success")
}
// Call the middleware with a bad request (no Authorization header)
middlewareHandler := middleware(handler)
err := middlewareHandler(c)
// Verify that our error handler was called
assert.NotNil(t, capturedError)
assert.NotNil(t, err)
// The error should be an HTTP error
httpErr, ok := err.(*echo.HTTPError)
assert.True(t, ok)
assert.Equal(t, http.StatusUnauthorized, httpErr.Code)
}
// TestJWT tests the JWT function which creates middleware with default config
func TestJWT(t *testing.T) {
// Create a new Echo instance
e := echo.New()
// Create a simple test endpoint with the JWT middleware
e.GET("/protected", func(c echo.Context) error {
return c.String(http.StatusOK, "success")
}, rbac.JWT(createMockKeyProvider()))
// Create a test request with an invalid token (format doesn't matter for this test)
// The mock key provider will accept any token
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
req.Header.Set("Authorization", "Bearer valid-token")
rec := httptest.NewRecorder()
// Process the request (this will fail because our mock doesn't properly validate)
e.ServeHTTP(rec, req)
// The response status should reflect whether validation succeeded
// In a real test, this would depend on token validity
// Here, we expect an error because our mock key provider doesn't correctly
// validate the token structure
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
// TestJWTWithMissingHeader tests JWT middleware with a missing Authorization header
func TestJWTWithMissingHeader(t *testing.T) {
// Create a new Echo instance
e := echo.New()
// Create a simple test endpoint with the JWT middleware
e.GET("/protected", func(c echo.Context) error {
return c.String(http.StatusOK, "success")
}, rbac.JWT(createMockKeyProvider()))
// Create a test request with no Authorization header
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
rec := httptest.NewRecorder()
// Process the request
e.ServeHTTP(rec, req)
// This should fail with a 401 Unauthorized
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}
-28
View File
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDX5vunPVZ3fVNY
dwJzOvTsgoHWEQlkXs/FSjHz90HzLCloZ8/g7yqIEPwmxmHHkHVjMJryyhzoOhnU
6OYuDDFiTlWL6UFX74+EC/bVDVFWOU016R6WZX+paeIn7JV7pvovoG+WCRgaypUy
rJ4FGAMNel8uuq1KCrhV5UsQ6e6IGJ3bry+mRp9+Pqk91viWj1JVc1t+t2IfyJMY
/Hka0FiobnIAVSQPrI6ULoZul80ndB26mK2WRsr6A+S/DVHZLJ2qvtxrtdqXsK+m
tr+MKrBWFhcnMrNkYCCTEFWC4TiyYIXWGWhHSNj87CIaboggQ2ybLvAiulFvrgf8
ES6LyRj3AgMBAAECggEAZ7MkMGG/xEjH3XfcD2jD901/+0fXkQQRG5vVfm7GmHwf
r2wdZta5QP2XfzBOCsKR/4B7DB6T3974RVFQLdHhbmxdnoP8xLXl4vC0MATjilyf
f0NnU6mQtdiLrc1uxyOei32t2wynLUccfmh2xc+Qt8qNKS60yRl5DJjDg245CdiW
ZhN40svLKfeVPallv2zrIZRbySEWXsCZRzBE+CYNrdRTWXSp45xCrRn+lseUxQXH
n+VoaP2KzHGgE4Lv5476EtVAq9G1yVN6Z0+8xWOiG9s+X3BNKPXWP4BGvg9tjBi0
77U/hYrytMEPu8RV0ZR0zvlFVItqjujdKOJ9zJWTIQKBgQD/shNiXwCG/Ywrvs/g
lWNkvM+8iYyALtCuOR7rsuR0QP0OnC9HsZbttOwMwiC8yJiMZ7DuBWp7iqfjmPs7
90dTxn4iQNkdoUE1DrobViPp+7pWIHqDH82r/Gy3nwMXLt/UFxi85lYd1Oo3us9C
ICvhFmzGAzIfiTKTXIJqJF9EqQKBgQDYKMe2ACP4rGVmJTSTkMn9c22gDTeCMEgI
dvlZOS7PJXAsznlnpTH+KLEyJhRenj08VxO6LcXI1urTe5G5aPStutBzods2X/iM
i1CmtOzeRGOkDVmqdYGgk0SXvTQg/z9MnwYCv+a904LQdMQ7TGgaoCYMJc3qWLO7
p+/qGSZUnwKBgBvZZ2cVddc+EmBJXhbV7odwUSf1y0nCz5PKQOXnDB7lXSqUNEoY
u5mUVQlms24cYxEX0ht6l4hxJ6wQY3y6iBhFzEMq0Pr7L0D6I6cKkMrRUhBDZVSW
yC3tRmIRfaKuxk4xXc5lQAfrwr7jJ+PJ4T2Y1awTeQgaR1npf4LUB1RRAoGBAL+t
GbrX0Q33wUqcf0zDPXoT2wfr8GbvbVCkP2PRAyMIrbnttVYk9HnNl6NChRmJ8/8H
sCSN5i679StnDcd9vEo5uBJxWjOTUpE+EFxjXw+RUVHtzK8M18+OB2sOiaUg8f59
nRTfGjsFzaAPitqSXFYP4O0wsLG3ylkDCAlsF8M9AoGALYTvOBdGJn/31XazCSeS
v2/iYnn41aw2DzxEYIn+QfkKH7Ctc39dfYV1jrhoI7m2T90GJKYfiZidd27fzmib
aPGQhCRFS7NPj2wI4kAFftRRtowIzHwEvVi1VvKWbyZII04FLU6aNVWrZE/E+2f5
/4AmcI3dqS5PhGQEystcvhY=
-----END PRIVATE KEY-----
-9
View File
@@ -1,9 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1+b7pz1Wd31TWHcCczr0
7IKB1hEJZF7PxUox8/dB8ywpaGfP4O8qiBD8JsZhx5B1YzCa8soc6DoZ1OjmLgwx
Yk5Vi+lBV++PhAv21Q1RVjlNNekelmV/qWniJ+yVe6b6L6BvlgkYGsqVMqyeBRgD
DXpfLrqtSgq4VeVLEOnuiBid268vpkaffj6pPdb4lo9SVXNbfrdiH8iTGPx5GtBY
qG5yAFUkD6yOlC6GbpfNJ3QdupitlkbK+gPkvw1R2Sydqr7ca7Xal7Cvpra/jCqw
VhYXJzKzZGAgkxBVguE4smCF1hloR0jY/OwiGm6IIENsmy7wIrpRb64H/BEui8kY
9wIDAQAB
-----END PUBLIC KEY-----
+4
View File
@@ -41,6 +41,10 @@ func TestInitializeConfig(t *testing.T) {
"SUB_FIELD1:": "value1",
"SUB_FIELD2": "42",
"PWD": "/foo",
"COGNITO_USER_POOL_ID": "poolid",
"COGNITO_CLIENT_SECRET": "secret",
"COGNITO_DOMAIN": "domain",
"COGNITO_CLIENT_ID": "clientid",
},
wantErr: false,
},
+5 -1
View File
@@ -26,6 +26,10 @@ func TestPrintConfigRecursive(t *testing.T) {
"AWS_ACCESS_KEY_ID": "key",
"AWS_SECRET_ACCESS_KEY": "password",
"AWS_REGION": "region",
"COGNITO_USER_POOL_ID": "poolid",
"COGNITO_CLIENT_SECRET": "secret",
"COGNITO_DOMAIN": "domain",
"COGNITO_CLIENT_ID": "clientid",
"PWD": "/foo", // Only include vars needed by BaseConfig
}
@@ -59,7 +63,7 @@ func TestPrintConfigRecursive(t *testing.T) {
// Check that each environment variable value appears in the logs
// except for sensitive values which should be masked
for envKey, envValue := range envVars {
if envKey == "PGPASSWORD" || envKey == "AWS_SECRET_ACCESS_KEY" {
if envKey == "PGPASSWORD" || envKey == "AWS_SECRET_ACCESS_KEY" || envKey == "COGNITO_CLIENT_SECRET" {
// Password should be masked
if strings.Contains(logOutput, envValue) {
t.Errorf("Full password should not appear in logs: %s", logOutput)
+4
View File
@@ -50,6 +50,10 @@ func createContainer(t testing.TB, ctx context.Context, cfg *containerConfig) (t
"AWS_ENDPOINT_URL_TEXTRACT": cfg.MockHTTP,
"AWS_S3_USE_PATH_STYLE": strconv.FormatBool(true),
"LOG_LEVEL": "DEBUG",
"COGNITO_USER_POOL_ID": "coguserpoolid",
"COGNITO_CLIENT_SECRET": "cogsecret",
"COGNITO_DOMAIN": "cogdomain",
"COGNITO_CLIENT_ID": "clientid",
}
if cfg.Env != nil {
for k, v := range cfg.Env {