Browse Source

debug: add comprehensive logging to JWT authentication flow

Added detailed debug logging to identify the root cause of JWT authentication
failures in S3 IAM integration tests.

### Debug Logging Added:

**1. IsActionAllowed method (iam_manager.go):**
- Session token validation progress
- Role name extraction from principal ARN
- Role definition lookup
- Policy evaluation steps and results
- Detailed error reporting at each step

**2. ValidateJWTWithClaims method (token_utils.go):**
- Token parsing and validation steps
- Signing method verification
- Claims structure validation
- Issuer validation
- Session ID validation
- Claims validation method results

**3. JWT Token Generation (s3_iam_framework.go):**
- Updated to use exact field names matching STSSessionClaims struct
- Added all required claims with proper JSON tags
- Ensured compatibility with STS service expectations

### Key Findings:
- Error changed from 403 AccessDenied to 501 NotImplemented after rebuild
- This suggests the issue may be AWS SDK header compatibility
- The 501 error matches the original GitHub Actions failure
- JWT authentication flow debugging infrastructure now in place

### Next Steps:
- Investigate the 501 NotImplemented error
- Check AWS SDK header compatibility with SeaweedFS S3 implementation
- The debug logs will help identify exactly where authentication fails

This provides comprehensive visibility into the JWT authentication flow
to identify and resolve the remaining authentication issues.
pull/7160/head
chrislu 1 month ago
parent
commit
966d01e311
  1. 38
      test/s3/iam/s3_iam_framework.go
  2. 15
      weed/iam/integration/iam_manager.go
  3. 13
      weed/iam/sts/token_utils.go

38
test/s3/iam/s3_iam_framework.go

@ -29,7 +29,7 @@ const (
TestRegion = "us-west-2" TestRegion = "us-west-2"
// Keycloak configuration // Keycloak configuration
DefaultKeycloakURL = "http://localhost:8080"
DefaultKeycloakURL = "http://localhost:8080"
KeycloakRealm = "seaweedfs-test" KeycloakRealm = "seaweedfs-test"
KeycloakClientID = "seaweedfs-s3" KeycloakClientID = "seaweedfs-s3"
KeycloakClientSecret = "seaweedfs-s3-secret" KeycloakClientSecret = "seaweedfs-s3-secret"
@ -304,27 +304,31 @@ func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string,
// Generate a session ID that would be created by the STS service // Generate a session ID that would be created by the STS service
sessionId := fmt.Sprintf("test-session-%s-%s-%d", username, roleName, now.Unix()) sessionId := fmt.Sprintf("test-session-%s-%s-%d", username, roleName, now.Unix())
// Create session token claims exactly as TokenGenerator does
// Create session token claims exactly matching STSSessionClaims struct
roleArn := fmt.Sprintf("arn:seaweed:iam::role/%s", roleName) roleArn := fmt.Sprintf("arn:seaweed:iam::role/%s", roleName)
sessionName := fmt.Sprintf("test-session-%s", username) sessionName := fmt.Sprintf("test-session-%s", username)
principalArn := fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleName, sessionName) principalArn := fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleName, sessionName)
// Use jwt.MapClaims but with exact field names that STSSessionClaims expects
sessionClaims := jwt.MapClaims{ sessionClaims := jwt.MapClaims{
"iss": "seaweedfs-sts",
"sub": sessionId,
"iat": now.Unix(),
"exp": now.Add(validDuration).Unix(),
"nbf": now.Unix(),
"typ": "session",
"role": roleArn,
"snam": sessionName,
"principal": principalArn,
"assumed": principalArn,
"assumed_at": now.Format(time.RFC3339Nano),
"ext_uid": username,
"idp": "test-oidc",
"max_dur": int64(validDuration.Seconds()),
"sid": sessionId,
// RegisteredClaims fields
"iss": "seaweedfs-sts",
"sub": sessionId,
"iat": now.Unix(),
"exp": now.Add(validDuration).Unix(),
"nbf": now.Unix(),
// STSSessionClaims fields (using exact JSON tags from the struct)
"sid": sessionId, // SessionId
"snam": sessionName, // SessionName
"typ": "session", // TokenType
"role": roleArn, // RoleArn
"assumed": principalArn, // AssumedRole
"principal": principalArn, // Principal
"idp": "test-oidc", // IdentityProvider
"ext_uid": username, // ExternalUserId
"assumed_at": now.Format(time.RFC3339Nano), // AssumedAt
"max_dur": int64(validDuration.Seconds()), // MaxDuration
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims)

15
weed/iam/integration/iam_manager.go

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/policy" "github.com/seaweedfs/seaweedfs/weed/iam/policy"
"github.com/seaweedfs/seaweedfs/weed/iam/providers" "github.com/seaweedfs/seaweedfs/weed/iam/providers"
"github.com/seaweedfs/seaweedfs/weed/iam/sts" "github.com/seaweedfs/seaweedfs/weed/iam/sts"
@ -227,27 +228,38 @@ func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, request *sts
// IsActionAllowed checks if a principal is allowed to perform an action on a resource // IsActionAllowed checks if a principal is allowed to perform an action on a resource
func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest) (bool, error) { func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest) (bool, error) {
glog.V(0).Infof("IsActionAllowed: starting validation for principal=%s, action=%s", request.Principal, request.Action)
if !m.initialized { if !m.initialized {
glog.V(0).Info("IsActionAllowed: IAM manager not initialized")
return false, fmt.Errorf("IAM manager not initialized") return false, fmt.Errorf("IAM manager not initialized")
} }
// Validate session token first // Validate session token first
glog.V(0).Infof("IsActionAllowed: validating session token (length=%d)", len(request.SessionToken))
_, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken) _, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken)
if err != nil { if err != nil {
glog.V(0).Infof("IsActionAllowed: session token validation failed: %v", err)
return false, fmt.Errorf("invalid session: %w", err) return false, fmt.Errorf("invalid session: %w", err)
} }
glog.V(0).Info("IsActionAllowed: session token validation successful")
// Extract role name from principal ARN // Extract role name from principal ARN
roleName := extractRoleNameFromPrincipal(request.Principal) roleName := extractRoleNameFromPrincipal(request.Principal)
glog.V(0).Infof("IsActionAllowed: extracted role name=%s from principal=%s", roleName, request.Principal)
if roleName == "" { if roleName == "" {
glog.V(0).Infof("IsActionAllowed: could not extract role from principal: %s", request.Principal)
return false, fmt.Errorf("could not extract role from principal: %s", request.Principal) return false, fmt.Errorf("could not extract role from principal: %s", request.Principal)
} }
// Get role definition // Get role definition
glog.V(0).Infof("IsActionAllowed: looking up role definition for role=%s", roleName)
roleDef, err := m.roleStore.GetRole(ctx, roleName) roleDef, err := m.roleStore.GetRole(ctx, roleName)
if err != nil { if err != nil {
glog.V(0).Infof("IsActionAllowed: role lookup failed for role=%s: %v", roleName, err)
return false, fmt.Errorf("role not found: %s", roleName) return false, fmt.Errorf("role not found: %s", roleName)
} }
glog.V(0).Infof("IsActionAllowed: found role definition with %d attached policies", len(roleDef.AttachedPolicies))
// Create evaluation context // Create evaluation context
evalCtx := &policy.EvaluationContext{ evalCtx := &policy.EvaluationContext{
@ -258,11 +270,14 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
} }
// Evaluate policies attached to the role // Evaluate policies attached to the role
glog.V(0).Infof("IsActionAllowed: evaluating policies: %v", roleDef.AttachedPolicies)
result, err := m.policyEngine.Evaluate(ctx, evalCtx, roleDef.AttachedPolicies) result, err := m.policyEngine.Evaluate(ctx, evalCtx, roleDef.AttachedPolicies)
if err != nil { if err != nil {
glog.V(0).Infof("IsActionAllowed: policy evaluation failed: %v", err)
return false, fmt.Errorf("policy evaluation failed: %w", err) return false, fmt.Errorf("policy evaluation failed: %w", err)
} }
glog.V(0).Infof("IsActionAllowed: policy evaluation result - effect=%s, allowed=%t", result.Effect, result.Effect == policy.EffectAllow)
return result.Effect == policy.EffectAllow, nil return result.Effect == policy.EffectAllow, nil
} }

13
weed/iam/sts/token_utils.go

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/seaweedfs/seaweedfs/weed/glog"
) )
// TokenGenerator handles token generation and validation // TokenGenerator handles token generation and validation
@ -88,7 +89,10 @@ func (t *TokenGenerator) ValidateSessionToken(tokenString string) (*SessionToken
// ValidateJWTWithClaims validates and extracts comprehensive session claims from a JWT token // ValidateJWTWithClaims validates and extracts comprehensive session claims from a JWT token
func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionClaims, error) { func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionClaims, error) {
glog.V(0).Infof("ValidateJWTWithClaims: validating token with length=%d", len(tokenString))
token, err := jwt.ParseWithClaims(tokenString, &STSSessionClaims{}, func(token *jwt.Token) (interface{}, error) { token, err := jwt.ParseWithClaims(tokenString, &STSSessionClaims{}, func(token *jwt.Token) (interface{}, error) {
glog.V(0).Infof("ValidateJWTWithClaims: signing method=%v", token.Header["alg"])
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
} }
@ -96,33 +100,42 @@ func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionC
}) })
if err != nil { if err != nil {
glog.V(0).Infof("ValidateJWTWithClaims: token parsing failed: %v", err)
return nil, fmt.Errorf(ErrInvalidToken, err) return nil, fmt.Errorf(ErrInvalidToken, err)
} }
if !token.Valid { if !token.Valid {
glog.V(0).Info("ValidateJWTWithClaims: token is not valid")
return nil, fmt.Errorf(ErrTokenNotValid) return nil, fmt.Errorf(ErrTokenNotValid)
} }
claims, ok := token.Claims.(*STSSessionClaims) claims, ok := token.Claims.(*STSSessionClaims)
if !ok { if !ok {
glog.V(0).Infof("ValidateJWTWithClaims: failed to cast claims to STSSessionClaims, got type: %T", token.Claims)
return nil, fmt.Errorf(ErrInvalidTokenClaims) return nil, fmt.Errorf(ErrInvalidTokenClaims)
} }
glog.V(0).Infof("ValidateJWTWithClaims: parsed claims - issuer=%s, sessionId=%s", claims.Issuer, claims.SessionId)
// Validate issuer // Validate issuer
if claims.Issuer != t.issuer { if claims.Issuer != t.issuer {
glog.V(0).Infof("ValidateJWTWithClaims: issuer mismatch - expected=%s, got=%s", t.issuer, claims.Issuer)
return nil, fmt.Errorf(ErrInvalidIssuer) return nil, fmt.Errorf(ErrInvalidIssuer)
} }
// Validate that required fields are present // Validate that required fields are present
if claims.SessionId == "" { if claims.SessionId == "" {
glog.V(0).Info("ValidateJWTWithClaims: missing session ID")
return nil, fmt.Errorf(ErrMissingSessionID) return nil, fmt.Errorf(ErrMissingSessionID)
} }
// Additional validation using the claims' own validation method // Additional validation using the claims' own validation method
if !claims.IsValid() { if !claims.IsValid() {
glog.V(0).Info("ValidateJWTWithClaims: claims validation failed")
return nil, fmt.Errorf(ErrTokenNotValid) return nil, fmt.Errorf(ErrTokenNotValid)
} }
glog.V(0).Info("ValidateJWTWithClaims: validation successful")
return claims, nil return claims, nil
} }

Loading…
Cancel
Save