fix tests
add new cognito vars
This commit is contained in:
@@ -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"}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user