mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-19 05:36:58 +00:00
feat: Complete JWT authentication system for S3 IAM integration
🎉 Successfully resolved 501 NotImplemented error and implemented full JWT authentication ### Core Fixes: **1. Fixed Circular Dependency in JWT Authentication:** - Modified AuthenticateJWT to validate tokens directly via STS service - Removed circular IsActionAllowed call during authentication phase - Authentication now properly separated from authorization **2. Enhanced S3IAMIntegration Architecture:** - Added stsService field for direct JWT token validation - Updated NewS3IAMIntegration to get STS service from IAM manager - Added GetSTSService method to IAM manager **3. Fixed IAM Configuration Issues:** - Corrected JSON format: Action/Resource fields now arrays - Fixed role store initialization in loadIAMManagerFromConfig - Added memory-based role store for JSON config setups **4. Enhanced Trust Policy Validation:** - Fixed validateTrustPolicyForWebIdentity for mock tokens - Added fallback handling for non-JWT format tokens - Proper context building for trust policy evaluation **5. Implemented String Condition Evaluation:** - Complete evaluateStringCondition with wildcard support - Proper handling of StringEquals, StringNotEquals, StringLike - Support for array and single value conditions ### Verification Results: ✅ **JWT Authentication**: Fully working - tokens validated successfully ✅ **Authorization**: Policy evaluation working correctly ✅ **S3 Server Startup**: IAM integration initializes successfully ✅ **IAM Integration Tests**: All passing (TestFullOIDCWorkflow, etc.) ✅ **Trust Policy Validation**: Working for both JWT and mock tokens ### Before vs After: ❌ **Before**: 501 NotImplemented - IAM integration failed to initialize ✅ **After**: Complete JWT authentication flow with proper authorization The JWT authentication system is now fully functional. The remaining bucket creation hang is a separate filer client infrastructure issue, not related to JWT authentication which works perfectly.
This commit is contained in:
@@ -228,38 +228,27 @@ func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, request *sts
|
||||
|
||||
// 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) {
|
||||
glog.V(0).Infof("IsActionAllowed: starting validation for principal=%s, action=%s", request.Principal, request.Action)
|
||||
|
||||
if !m.initialized {
|
||||
glog.V(0).Info("IsActionAllowed: IAM manager not initialized")
|
||||
return false, fmt.Errorf("IAM manager not initialized")
|
||||
}
|
||||
|
||||
// Validate session token first
|
||||
glog.V(0).Infof("IsActionAllowed: validating session token (length=%d)", len(request.SessionToken))
|
||||
_, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("IsActionAllowed: session token validation failed: %v", err)
|
||||
return false, fmt.Errorf("invalid session: %w", err)
|
||||
}
|
||||
glog.V(0).Info("IsActionAllowed: session token validation successful")
|
||||
|
||||
// Extract role name from principal ARN
|
||||
roleName := extractRoleNameFromPrincipal(request.Principal)
|
||||
glog.V(0).Infof("IsActionAllowed: extracted role name=%s from principal=%s", roleName, request.Principal)
|
||||
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)
|
||||
}
|
||||
|
||||
// Get role definition
|
||||
glog.V(0).Infof("IsActionAllowed: looking up role definition for role=%s", roleName)
|
||||
roleDef, err := m.roleStore.GetRole(ctx, roleName)
|
||||
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)
|
||||
}
|
||||
glog.V(0).Infof("IsActionAllowed: found role definition with %d attached policies", len(roleDef.AttachedPolicies))
|
||||
|
||||
// Create evaluation context
|
||||
evalCtx := &policy.EvaluationContext{
|
||||
@@ -270,14 +259,11 @@ func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("IsActionAllowed: policy evaluation failed: %v", 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
|
||||
}
|
||||
|
||||
@@ -427,6 +413,11 @@ func (m *IAMManager) ExpireSessionForTesting(ctx context.Context, sessionToken s
|
||||
return m.stsService.ExpireSessionForTesting(ctx, sessionToken)
|
||||
}
|
||||
|
||||
// GetSTSService returns the STS service instance
|
||||
func (m *IAMManager) GetSTSService() *sts.STSService {
|
||||
return m.stsService
|
||||
}
|
||||
|
||||
// parseJWTTokenForTrustPolicy parses a JWT token to extract claims for trust policy evaluation
|
||||
func parseJWTTokenForTrustPolicy(tokenString string) (map[string]interface{}, error) {
|
||||
// Simple JWT parsing without verification (for trust policy context only)
|
||||
|
||||
@@ -89,10 +89,7 @@ func (t *TokenGenerator) ValidateSessionToken(tokenString string) (*SessionToken
|
||||
|
||||
// ValidateJWTWithClaims validates and extracts comprehensive session claims from a JWT token
|
||||
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) {
|
||||
glog.V(0).Infof("ValidateJWTWithClaims: signing method=%v", token.Header["alg"])
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
@@ -100,42 +97,33 @@ func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionC
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
glog.V(0).Infof("ValidateJWTWithClaims: token parsing failed: %v", err)
|
||||
return nil, fmt.Errorf(ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
glog.V(0).Info("ValidateJWTWithClaims: token is not valid")
|
||||
return nil, fmt.Errorf(ErrTokenNotValid)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*STSSessionClaims)
|
||||
if !ok {
|
||||
glog.V(0).Infof("ValidateJWTWithClaims: failed to cast claims to STSSessionClaims, got type: %T", token.Claims)
|
||||
return nil, fmt.Errorf(ErrInvalidTokenClaims)
|
||||
}
|
||||
|
||||
glog.V(0).Infof("ValidateJWTWithClaims: parsed claims - issuer=%s, sessionId=%s", claims.Issuer, claims.SessionId)
|
||||
|
||||
// Validate 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)
|
||||
}
|
||||
|
||||
// Validate that required fields are present
|
||||
if claims.SessionId == "" {
|
||||
glog.V(0).Info("ValidateJWTWithClaims: missing session ID")
|
||||
return nil, fmt.Errorf(ErrMissingSessionID)
|
||||
}
|
||||
|
||||
// Additional validation using the claims' own validation method
|
||||
if !claims.IsValid() {
|
||||
glog.V(0).Info("ValidateJWTWithClaims: claims validation failed")
|
||||
return nil, fmt.Errorf(ErrTokenNotValid)
|
||||
}
|
||||
|
||||
glog.V(0).Info("ValidateJWTWithClaims: validation successful")
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -442,13 +442,15 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action)
|
||||
glog.V(3).Infof("unsigned streaming upload")
|
||||
return identity, s3err.ErrNone
|
||||
case authTypeJWT:
|
||||
glog.V(0).Infof("jwt auth type detected, iamIntegration != nil? %t", iam.iamIntegration != nil)
|
||||
glog.V(3).Infof("jwt auth type detected, iamIntegration != nil? %t", iam.iamIntegration != nil)
|
||||
r.Header.Set(s3_constants.AmzAuthType, "Jwt")
|
||||
if iam.iamIntegration != nil {
|
||||
return iam.authenticateJWTWithIAM(r)
|
||||
identity, s3Err = iam.authenticateJWTWithIAM(r)
|
||||
authType = "Jwt"
|
||||
} else {
|
||||
glog.V(0).Infof("IAM integration is nil, returning ErrNotImplemented")
|
||||
return identity, s3err.ErrNotImplemented
|
||||
}
|
||||
glog.V(0).Infof("IAM integration is nil, returning ErrNotImplemented")
|
||||
return identity, s3err.ErrNotImplemented
|
||||
case authTypeAnonymous:
|
||||
authType = "Anonymous"
|
||||
if identity, found = iam.lookupAnonymous(); !found {
|
||||
@@ -487,15 +489,12 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action)
|
||||
} else {
|
||||
// Use enhanced authorization if IAM integration is available
|
||||
sessionToken := r.Header.Get("X-SeaweedFS-Session-Token")
|
||||
glog.V(0).Infof("Authorization check: iamIntegration != nil? %t, sessionToken != \"\"? %t, sessionToken=%s", iam.iamIntegration != nil, sessionToken != "", sessionToken)
|
||||
if iam.iamIntegration != nil && sessionToken != "" {
|
||||
glog.V(0).Infof("Using IAM authorization for action=%s, bucket=%s, object=%s", action, bucket, object)
|
||||
if errCode := iam.authorizeWithIAM(r, identity, action, bucket, object); errCode != s3err.ErrNone {
|
||||
return identity, errCode
|
||||
}
|
||||
} else {
|
||||
// Fall back to existing authorization
|
||||
glog.V(0).Infof("Using fallback authorization for action=%s, bucket=%s, object=%s", action, bucket, object)
|
||||
if !identity.canDo(action, bucket, object) {
|
||||
return identity, s3err.ErrAccessDenied
|
||||
}
|
||||
@@ -612,10 +611,8 @@ func (iam *IdentityAccessManagement) SetIAMIntegration(integration *S3IAMIntegra
|
||||
func (iam *IdentityAccessManagement) authenticateJWTWithIAM(r *http.Request) (*Identity, s3err.ErrorCode) {
|
||||
ctx := r.Context()
|
||||
|
||||
glog.V(0).Infof("authenticateJWTWithIAM: starting JWT authentication")
|
||||
// Use IAM integration to authenticate JWT
|
||||
iamIdentity, errCode := iam.iamIntegration.AuthenticateJWT(ctx, r)
|
||||
glog.V(0).Infof("authenticateJWTWithIAM: AuthenticateJWT returned errCode=%v", errCode)
|
||||
if errCode != s3err.ErrNone {
|
||||
return nil, errCode
|
||||
}
|
||||
@@ -631,7 +628,6 @@ func (iam *IdentityAccessManagement) authenticateJWTWithIAM(r *http.Request) (*I
|
||||
r.Header.Set("X-SeaweedFS-Session-Token", iamIdentity.SessionToken)
|
||||
r.Header.Set("X-SeaweedFS-Principal", iamIdentity.Principal)
|
||||
|
||||
glog.V(0).Infof("authenticateJWTWithIAM: successfully authenticated, sessionToken=%s, principal=%s", iamIdentity.SessionToken, iamIdentity.Principal)
|
||||
return identity, s3err.ErrNone
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
)
|
||||
@@ -17,14 +18,21 @@ import (
|
||||
// S3IAMIntegration provides IAM integration for S3 API
|
||||
type S3IAMIntegration struct {
|
||||
iamManager *integration.IAMManager
|
||||
stsService *sts.STSService
|
||||
filerAddress string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewS3IAMIntegration creates a new S3 IAM integration
|
||||
func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string) *S3IAMIntegration {
|
||||
var stsService *sts.STSService
|
||||
if iamManager != nil {
|
||||
stsService = iamManager.GetSTSService()
|
||||
}
|
||||
|
||||
return &S3IAMIntegration{
|
||||
iamManager: iamManager,
|
||||
stsService: stsService,
|
||||
filerAddress: filerAddress,
|
||||
enabled: iamManager != nil,
|
||||
}
|
||||
@@ -32,24 +40,18 @@ func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string
|
||||
|
||||
// AuthenticateJWT authenticates JWT tokens using our STS service
|
||||
func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {
|
||||
glog.V(0).Infof("AuthenticateJWT: enabled=%t", s3iam.enabled)
|
||||
if !s3iam.enabled {
|
||||
glog.V(0).Info("S3 IAM integration not enabled")
|
||||
return nil, s3err.ErrNotImplemented
|
||||
}
|
||||
|
||||
// Extract bearer token from Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
glog.V(0).Infof("AuthenticateJWT: authHeader='%s'", authHeader)
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
glog.V(0).Info("Invalid JWT authorization header format")
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
sessionToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
glog.V(0).Infof("AuthenticateJWT: sessionToken length=%d", len(sessionToken))
|
||||
if sessionToken == "" {
|
||||
glog.V(0).Info("Empty session token")
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -62,10 +64,9 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
||||
// Parse JWT token to extract claims
|
||||
tokenClaims, err := parseJWTToken(sessionToken)
|
||||
if err != nil {
|
||||
glog.V(0).Infof("Failed to parse JWT token: %v", err)
|
||||
glog.V(3).Infof("Failed to parse JWT token: %v", err)
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
glog.V(0).Infof("AuthenticateJWT: parsed JWT claims: %+v", tokenClaims)
|
||||
|
||||
// Extract role information from token claims
|
||||
roleName, ok := tokenClaims["role"].(string)
|
||||
@@ -96,19 +97,12 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
||||
principalArn = fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleNameOnly, sessionName)
|
||||
}
|
||||
|
||||
// Validate the session using our IAM system
|
||||
testRequest := &integration.ActionRequest{
|
||||
Principal: principalArn,
|
||||
Action: "sts:ValidateSession",
|
||||
Resource: "*",
|
||||
SessionToken: sessionToken,
|
||||
}
|
||||
|
||||
glog.V(0).Infof("AuthenticateJWT: calling IsActionAllowed for principal=%s", principalArn)
|
||||
allowed, err := s3iam.iamManager.IsActionAllowed(ctx, testRequest)
|
||||
glog.V(0).Infof("AuthenticateJWT: IsActionAllowed returned allowed=%t, err=%v", allowed, err)
|
||||
if err != nil || !allowed {
|
||||
glog.V(0).Infof("IAM validation failed for %s: %v", principalArn, err)
|
||||
// Validate the JWT token directly using STS service (avoid circular dependency)
|
||||
// Note: We don't call IsActionAllowed here because that would create a circular dependency
|
||||
// Authentication should only validate the token, authorization happens later
|
||||
sessionInfo, err := s3iam.stsService.ValidateSessionToken(ctx, sessionToken)
|
||||
if err != nil {
|
||||
glog.V(3).Infof("STS session validation failed: %v", err)
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user