diff --git a/weed/iam/integration/iam_integration_test.go b/weed/iam/integration/iam_integration_test.go index 48d3b8149..c5de6ec60 100644 --- a/weed/iam/integration/iam_integration_test.go +++ b/weed/iam/integration/iam_integration_test.go @@ -349,14 +349,18 @@ func setupIntegratedIAMSystem(t *testing.T) *IAMManager { // Configure and initialize config := &IAMConfig{ STS: &sts.STSConfig{ - TokenDuration: time.Hour, - MaxSessionLength: time.Hour * 12, - Issuer: "test-sts", - SigningKey: []byte("test-signing-key-32-characters-long"), + TokenDuration: time.Hour, + MaxSessionLength: time.Hour * 12, + Issuer: "test-sts", + SigningKey: []byte("test-signing-key-32-characters-long"), + SessionStoreType: "memory", // Use memory for unit tests }, Policy: &policy.PolicyEngineConfig{ DefaultEffect: "Deny", - StoreType: "memory", + StoreType: "memory", // Use memory for unit tests + }, + Roles: &RoleStoreConfig{ + StoreType: "memory", // Use memory for unit tests }, } diff --git a/weed/iam/integration/iam_manager.go b/weed/iam/integration/iam_manager.go index 4f29cc4cd..095505dcf 100644 --- a/weed/iam/integration/iam_manager.go +++ b/weed/iam/integration/iam_manager.go @@ -174,7 +174,7 @@ func (m *IAMManager) CreateRole(ctx context.Context, roleName string, roleDef *R } // AssumeRoleWithWebIdentity assumes a role using web identity (OIDC) -func (m *IAMManager) AssumeRoleWithWebIdentity(ctx context.Context, filerAddress string, request *sts.AssumeRoleWithWebIdentityRequest) (*sts.AssumeRoleResponse, error) { +func (m *IAMManager) AssumeRoleWithWebIdentity(ctx context.Context, request *sts.AssumeRoleWithWebIdentityRequest) (*sts.AssumeRoleResponse, error) { if !m.initialized { return nil, fmt.Errorf("IAM manager not initialized") } @@ -194,11 +194,11 @@ func (m *IAMManager) AssumeRoleWithWebIdentity(ctx context.Context, filerAddress } // Use STS service to assume the role - return m.stsService.AssumeRoleWithWebIdentity(ctx, filerAddress, request) + return m.stsService.AssumeRoleWithWebIdentity(ctx, request) } // AssumeRoleWithCredentials assumes a role using credentials (LDAP) -func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, filerAddress string, request *sts.AssumeRoleWithCredentialsRequest) (*sts.AssumeRoleResponse, error) { +func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, request *sts.AssumeRoleWithCredentialsRequest) (*sts.AssumeRoleResponse, error) { if !m.initialized { return nil, fmt.Errorf("IAM manager not initialized") } @@ -218,17 +218,17 @@ func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, filerAddress } // Use STS service to assume the role - return m.stsService.AssumeRoleWithCredentials(ctx, filerAddress, request) + return m.stsService.AssumeRoleWithCredentials(ctx, request) } // IsActionAllowed checks if a principal is allowed to perform an action on a resource -func (m *IAMManager) IsActionAllowed(ctx context.Context, filerAddress string, request *ActionRequest) (bool, error) { +func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest) (bool, error) { if !m.initialized { return false, fmt.Errorf("IAM manager not initialized") } // Validate session token first - _, err := m.stsService.ValidateSessionToken(ctx, filerAddress, request.SessionToken) + _, err := m.stsService.ValidateSessionToken(ctx, request.SessionToken) if err != nil { return false, fmt.Errorf("invalid session: %w", err) } @@ -381,10 +381,10 @@ func indexOf(s, substr string) int { } // ExpireSessionForTesting manually expires a session for testing purposes -func (m *IAMManager) ExpireSessionForTesting(ctx context.Context, filerAddress string, sessionToken string) error { +func (m *IAMManager) ExpireSessionForTesting(ctx context.Context, sessionToken string) error { if !m.initialized { return fmt.Errorf("IAM manager not initialized") } - return m.stsService.ExpireSessionForTesting(ctx, filerAddress, sessionToken) + return m.stsService.ExpireSessionForTesting(ctx, sessionToken) } diff --git a/weed/iam/sts/cross_instance_token_test.go b/weed/iam/sts/cross_instance_token_test.go index 5b4803ce5..8ffdedb7b 100644 --- a/weed/iam/sts/cross_instance_token_test.go +++ b/weed/iam/sts/cross_instance_token_test.go @@ -21,11 +21,6 @@ func TestCrossInstanceTokenUsage(t *testing.T) { MaxSessionLength: 12 * time.Hour, Issuer: "distributed-sts-cluster", // SAME across all instances SigningKey: []byte(TestSigningKey32Chars), // SAME across all instances - SessionStoreType: StoreTypeMemory, // In production, this would be "filer" for true sharing - SessionStoreConfig: map[string]interface{}{ - ConfigFieldFilerAddress: "shared-filer:8888", - ConfigFieldBasePath: DefaultSessionBasePath, - }, Providers: []*ProviderConfig{ { Name: "company-oidc", @@ -100,7 +95,7 @@ func TestCrossInstanceTokenUsage(t *testing.T) { } // Instance A processes assume role request - responseFromA, err := instanceA.AssumeRoleWithWebIdentity(ctx, testFilerAddress, assumeRequest) + responseFromA, err := instanceA.AssumeRoleWithWebIdentity(ctx, assumeRequest) require.NoError(t, err, "Instance A should process assume role") sessionToken := responseFromA.Credentials.SessionToken @@ -114,14 +109,14 @@ func TestCrossInstanceTokenUsage(t *testing.T) { assert.NotNil(t, responseFromA.AssumedRoleUser, "Should have assumed role user") // Step 2: Use session token on Instance B (different instance) - sessionInfoFromB, err := instanceB.ValidateSessionToken(ctx, testFilerAddress, sessionToken) + sessionInfoFromB, err := instanceB.ValidateSessionToken(ctx, sessionToken) require.NoError(t, err, "Instance B should validate session token from Instance A") assert.Equal(t, assumeRequest.RoleSessionName, sessionInfoFromB.SessionName) assert.Equal(t, assumeRequest.RoleArn, sessionInfoFromB.RoleArn) // Step 3: Use same session token on Instance C (yet another instance) - sessionInfoFromC, err := instanceC.ValidateSessionToken(ctx, testFilerAddress, sessionToken) + sessionInfoFromC, err := instanceC.ValidateSessionToken(ctx, sessionToken) require.NoError(t, err, "Instance C should validate session token from Instance A") // All instances should return identical session information @@ -141,7 +136,7 @@ func TestCrossInstanceTokenUsage(t *testing.T) { RoleSessionName: "revocation-test-session", } - response, err := instanceA.AssumeRoleWithWebIdentity(ctx, testFilerAddress, assumeRequest) + response, err := instanceA.AssumeRoleWithWebIdentity(ctx, assumeRequest) require.NoError(t, err) sessionToken := response.Credentials.SessionToken @@ -183,9 +178,9 @@ func TestCrossInstanceTokenUsage(t *testing.T) { } // Should work on any instance - responseA, errA := instanceA.AssumeRoleWithWebIdentity(ctx, testFilerAddress, assumeRequest) - responseB, errB := instanceB.AssumeRoleWithWebIdentity(ctx, testFilerAddress, assumeRequest) - responseC, errC := instanceC.AssumeRoleWithWebIdentity(ctx, testFilerAddress, assumeRequest) + responseA, errA := instanceA.AssumeRoleWithWebIdentity(ctx, assumeRequest) + responseB, errB := instanceB.AssumeRoleWithWebIdentity(ctx, assumeRequest) + responseC, errC := instanceC.AssumeRoleWithWebIdentity(ctx, assumeRequest) require.NoError(t, errA, "Instance A should process OIDC token") require.NoError(t, errB, "Instance B should process OIDC token") @@ -376,7 +371,7 @@ func TestSTSRealWorldDistributedScenarios(t *testing.T) { DurationSeconds: int64ToPtr(7200), // 2 hours } - stsResponse, err := gateway1.AssumeRoleWithWebIdentity(ctx, testFilerAddress, assumeRequest) + stsResponse, err := gateway1.AssumeRoleWithWebIdentity(ctx, assumeRequest) require.NoError(t, err, "Gateway 1 should handle AssumeRole") sessionToken := stsResponse.Credentials.SessionToken diff --git a/weed/iam/sts/session_claims.go b/weed/iam/sts/session_claims.go new file mode 100644 index 000000000..ad38ec2cb --- /dev/null +++ b/weed/iam/sts/session_claims.go @@ -0,0 +1,146 @@ +package sts + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// STSSessionClaims represents comprehensive session information embedded in JWT tokens +// This eliminates the need for separate session storage by embedding all session +// metadata directly in the token itself - enabling true stateless operation +type STSSessionClaims struct { + jwt.RegisteredClaims + + // Session identification + SessionId string `json:"sid"` // session_id (abbreviated for smaller tokens) + TokenType string `json:"typ"` // token_type + + // Role information + RoleArn string `json:"role"` // role_arn + AssumedRole string `json:"assumed"` // assumed_role_user + Principal string `json:"principal"` // principal_arn + + // Authorization data + Policies []string `json:"pol,omitempty"` // policies (abbreviated) + + // Identity provider information + IdentityProvider string `json:"idp"` // identity_provider + ExternalUserId string `json:"ext_uid"` // external_user_id + ProviderIssuer string `json:"prov_iss"` // provider_issuer + + // Request context (optional, for policy evaluation) + RequestContext map[string]interface{} `json:"req_ctx,omitempty"` + + // Session metadata + AssumedAt time.Time `json:"assumed_at"` // when role was assumed + MaxDuration int64 `json:"max_dur,omitempty"` // maximum session duration in seconds +} + +// NewSTSSessionClaims creates new STS session claims with all required information +func NewSTSSessionClaims(sessionId, issuer string, expiresAt time.Time) *STSSessionClaims { + now := time.Now() + return &STSSessionClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: issuer, + Subject: sessionId, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expiresAt), + NotBefore: jwt.NewNumericDate(now), + }, + SessionId: sessionId, + TokenType: TokenTypeSession, + AssumedAt: now, + } +} + +// ToSessionInfo converts JWT claims back to SessionInfo structure +// This enables seamless integration with existing code expecting SessionInfo +func (c *STSSessionClaims) ToSessionInfo() *SessionInfo { + var expiresAt time.Time + if c.ExpiresAt != nil { + expiresAt = c.ExpiresAt.Time + } + + return &SessionInfo{ + SessionId: c.SessionId, + RoleArn: c.RoleArn, + AssumedRoleUser: c.AssumedRole, + Principal: c.Principal, + Policies: c.Policies, + ExpiresAt: expiresAt, + IdentityProvider: c.IdentityProvider, + ExternalUserId: c.ExternalUserId, + ProviderIssuer: c.ProviderIssuer, + RequestContext: c.RequestContext, + } +} + +// IsValid checks if the session claims are valid (not expired, etc.) +func (c *STSSessionClaims) IsValid() bool { + now := time.Now() + + // Check expiration + if c.ExpiresAt != nil && c.ExpiresAt.Before(now) { + return false + } + + // Check not-before + if c.NotBefore != nil && c.NotBefore.After(now) { + return false + } + + // Ensure required fields are present + if c.SessionId == "" || c.RoleArn == "" || c.Principal == "" { + return false + } + + return true +} + +// GetSessionId returns the session identifier +func (c *STSSessionClaims) GetSessionId() string { + return c.SessionId +} + +// GetExpiresAt returns the expiration time +func (c *STSSessionClaims) GetExpiresAt() time.Time { + if c.ExpiresAt != nil { + return c.ExpiresAt.Time + } + return time.Time{} +} + +// WithRoleInfo sets role-related information in the claims +func (c *STSSessionClaims) WithRoleInfo(roleArn, assumedRole, principal string) *STSSessionClaims { + c.RoleArn = roleArn + c.AssumedRole = assumedRole + c.Principal = principal + return c +} + +// WithPolicies sets the policies associated with this session +func (c *STSSessionClaims) WithPolicies(policies []string) *STSSessionClaims { + c.Policies = policies + return c +} + +// WithIdentityProvider sets identity provider information +func (c *STSSessionClaims) WithIdentityProvider(providerName, externalUserId, providerIssuer string) *STSSessionClaims { + c.IdentityProvider = providerName + c.ExternalUserId = externalUserId + c.ProviderIssuer = providerIssuer + return c +} + +// WithRequestContext sets request context for policy evaluation +func (c *STSSessionClaims) WithRequestContext(ctx map[string]interface{}) *STSSessionClaims { + c.RequestContext = ctx + return c +} + +// WithMaxDuration sets the maximum session duration +func (c *STSSessionClaims) WithMaxDuration(duration time.Duration) *STSSessionClaims { + c.MaxDuration = int64(duration.Seconds()) + return c +} diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index 7f6d25e87..de1aee9f2 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -10,11 +10,13 @@ import ( ) // STSService provides Security Token Service functionality +// This service is now completely stateless - all session information is embedded +// in JWT tokens, eliminating the need for session storage and enabling true +// distributed operation without shared state type STSService struct { config *STSConfig initialized bool providers map[string]providers.IdentityProvider - sessionStore SessionStore tokenGenerator *TokenGenerator } @@ -32,10 +34,6 @@ type STSConfig struct { // SigningKey is used to sign session tokens SigningKey []byte `json:"signingKey"` - // SessionStore configuration - SessionStoreType string `json:"sessionStoreType"` // memory, filer, redis - SessionStoreConfig map[string]interface{} `json:"sessionStoreConfig,omitempty"` - // Providers configuration - enables automatic provider loading Providers []*ProviderConfig `json:"providers,omitempty"` } @@ -144,12 +142,33 @@ type SessionInfo struct { // RoleArn is the ARN of the assumed role RoleArn string `json:"roleArn"` + // AssumedRoleUser contains information about the assumed role user + AssumedRoleUser string `json:"assumedRoleUser"` + + // Principal is the principal ARN + Principal string `json:"principal"` + // Subject is the subject identifier from the identity provider Subject string `json:"subject"` - // Provider is the identity provider used + // Provider is the identity provider used (legacy field) Provider string `json:"provider"` + // IdentityProvider is the identity provider used + IdentityProvider string `json:"identityProvider"` + + // ExternalUserId is the external user identifier from the provider + ExternalUserId string `json:"externalUserId"` + + // ProviderIssuer is the issuer from the identity provider + ProviderIssuer string `json:"providerIssuer"` + + // Policies are the policies associated with this session + Policies []string `json:"policies"` + + // RequestContext contains additional request context for policy evaluation + RequestContext map[string]interface{} `json:"requestContext,omitempty"` + // CreatedAt is when the session was created CreatedAt time.Time `json:"createdAt"` @@ -194,14 +213,7 @@ func (s *STSService) Initialize(config *STSConfig) error { s.config = config - // Initialize session store - sessionStore, err := s.createSessionStore(config) - if err != nil { - return fmt.Errorf("failed to initialize session store: %w", err) - } - s.sessionStore = sessionStore - - // Initialize token generator for JWT validation + // Initialize token generator for stateless JWT operations s.tokenGenerator = NewTokenGenerator(config.SigningKey, config.Issuer) // Load identity providers from configuration @@ -234,18 +246,6 @@ func (s *STSService) validateConfig(config *STSConfig) error { return nil } -// createSessionStore creates a session store based on configuration -func (s *STSService) createSessionStore(config *STSConfig) (SessionStore, error) { - switch config.SessionStoreType { - case "", DefaultStoreType: - return NewFilerSessionStore(config.SessionStoreConfig) - case StoreTypeMemory: - return NewMemorySessionStore(), nil - default: - return nil, fmt.Errorf(ErrUnsupportedStoreType, config.SessionStoreType) - } -} - // loadProvidersFromConfig loads identity providers from configuration func (s *STSService) loadProvidersFromConfig(config *STSConfig) error { if len(config.Providers) == 0 { @@ -300,7 +300,8 @@ func (s *STSService) RegisterProvider(provider providers.IdentityProvider) error } // AssumeRoleWithWebIdentity assumes a role using a web identity token (OIDC) -func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, filerAddress string, request *AssumeRoleWithWebIdentityRequest) (*AssumeRoleResponse, error) { +// This method is now completely stateless - all session information is embedded in the JWT token +func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *AssumeRoleWithWebIdentityRequest) (*AssumeRoleResponse, error) { if !s.initialized { return nil, fmt.Errorf(ErrSTSServiceNotInitialized) } @@ -341,45 +342,37 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, filerAddress return nil, fmt.Errorf("failed to generate credentials: %w", err) } - // Generate proper JWT session token using our TokenGenerator - jwtToken, err := s.tokenGenerator.GenerateSessionToken(sessionId, expiresAt) - if err != nil { - return nil, fmt.Errorf("failed to generate JWT session token: %w", err) - } - credentials.SessionToken = jwtToken - - // 5. Create session information - session := &SessionInfo{ - SessionId: sessionId, - SessionName: request.RoleSessionName, - RoleArn: request.RoleArn, - Subject: externalIdentity.UserID, - Provider: provider.Name(), - CreatedAt: time.Now(), - ExpiresAt: expiresAt, - Credentials: credentials, - } - - // 6. Store session information - if err := s.sessionStore.StoreSession(ctx, filerAddress, sessionId, session); err != nil { - return nil, fmt.Errorf("failed to store session: %w", err) - } - - // 7. Build and return response + // 5. Create comprehensive JWT session token with all session information embedded assumedRoleUser := &AssumedRoleUser{ AssumedRoleId: request.RoleArn, Arn: GenerateAssumedRoleArn(request.RoleArn, request.RoleSessionName), Subject: externalIdentity.UserID, } + // Create rich JWT claims with all session information + sessionClaims := NewSTSSessionClaims(sessionId, s.config.Issuer, expiresAt). + WithRoleInfo(request.RoleArn, assumedRoleUser.Arn, assumedRoleUser.Arn). + WithIdentityProvider(provider.Name(), externalIdentity.UserID, ""). + WithMaxDuration(sessionDuration) + + // Generate self-contained JWT token with all session information + jwtToken, err := s.tokenGenerator.GenerateJWTWithClaims(sessionClaims) + if err != nil { + return nil, fmt.Errorf("failed to generate JWT session token: %w", err) + } + credentials.SessionToken = jwtToken + + // 6. Build and return response (no session storage needed!) + return &AssumeRoleResponse{ Credentials: credentials, AssumedRoleUser: assumedRoleUser, }, nil } -// AssumeRoleWithCredentials assumes a role using username/password credentials -func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, filerAddress string, request *AssumeRoleWithCredentialsRequest) (*AssumeRoleResponse, error) { +// AssumeRoleWithCredentials assumes a role using username/password credentials +// This method is now completely stateless - all session information is embedded in the JWT token +func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, request *AssumeRoleWithCredentialsRequest) (*AssumeRoleResponse, error) { if !s.initialized { return nil, fmt.Errorf("STS service not initialized") } @@ -427,37 +420,28 @@ func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, filerAddress return nil, fmt.Errorf("failed to generate credentials: %w", err) } - // Generate proper JWT session token using our TokenGenerator - jwtToken, err := s.tokenGenerator.GenerateSessionToken(sessionId, expiresAt) - if err != nil { - return nil, fmt.Errorf("failed to generate JWT session token: %w", err) - } - tempCredentials.SessionToken = jwtToken - - // 6. Create session information - session := &SessionInfo{ - SessionId: sessionId, - SessionName: request.RoleSessionName, - RoleArn: request.RoleArn, - Subject: externalIdentity.UserID, - Provider: provider.Name(), - CreatedAt: time.Now(), - ExpiresAt: expiresAt, - Credentials: tempCredentials, - } - - // 7. Store session information - if err := s.sessionStore.StoreSession(ctx, filerAddress, sessionId, session); err != nil { - return nil, fmt.Errorf("failed to store session: %w", err) - } - - // 8. Build and return response + // 6. Create comprehensive JWT session token with all session information embedded assumedRoleUser := &AssumedRoleUser{ AssumedRoleId: request.RoleArn, Arn: GenerateAssumedRoleArn(request.RoleArn, request.RoleSessionName), Subject: externalIdentity.UserID, } + // Create rich JWT claims with all session information + sessionClaims := NewSTSSessionClaims(sessionId, s.config.Issuer, expiresAt). + WithRoleInfo(request.RoleArn, assumedRoleUser.Arn, assumedRoleUser.Arn). + WithIdentityProvider(provider.Name(), externalIdentity.UserID, ""). + WithMaxDuration(sessionDuration) + + // Generate self-contained JWT token with all session information + jwtToken, err := s.tokenGenerator.GenerateJWTWithClaims(sessionClaims) + if err != nil { + return nil, fmt.Errorf("failed to generate JWT session token: %w", err) + } + tempCredentials.SessionToken = jwtToken + + // 7. Build and return response (no session storage needed!) + return &AssumeRoleResponse{ Credentials: tempCredentials, AssumedRoleUser: assumedRoleUser, @@ -465,7 +449,8 @@ func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, filerAddress } // ValidateSessionToken validates a session token and returns session information -func (s *STSService) ValidateSessionToken(ctx context.Context, filerAddress string, sessionToken string) (*SessionInfo, error) { +// This method is now completely stateless - all session information is extracted from the JWT token +func (s *STSService) ValidateSessionToken(ctx context.Context, sessionToken string) (*SessionInfo, error) { if !s.initialized { return nil, fmt.Errorf(ErrSTSServiceNotInitialized) } @@ -474,28 +459,22 @@ func (s *STSService) ValidateSessionToken(ctx context.Context, filerAddress stri return nil, fmt.Errorf(ErrSessionTokenCannotBeEmpty) } - // Use token generator for proper JWT validation - claims, err := s.tokenGenerator.ValidateSessionToken(sessionToken) - if err != nil { - return nil, fmt.Errorf(ErrInvalidTokenFormat, err) - } - - // Retrieve session from store using session ID from claims - session, err := s.sessionStore.GetSession(ctx, filerAddress, claims.SessionId) + // Validate JWT and extract comprehensive session claims + claims, err := s.tokenGenerator.ValidateJWTWithClaims(sessionToken) if err != nil { return nil, fmt.Errorf(ErrSessionValidationFailed, err) } - // Additional validation - check expiration - if session.ExpiresAt.Before(time.Now()) { - return nil, fmt.Errorf("session has expired") - } - - return session, nil + // Convert JWT claims back to SessionInfo + // All session information is embedded in the JWT token itself + return claims.ToSessionInfo(), nil } -// RevokeSession revokes an active session -func (s *STSService) RevokeSession(ctx context.Context, filerAddress string, sessionToken string) error { +// RevokeSession validates a session token (stateless operation) +// Note: In a stateless JWT system, sessions cannot be revoked without a blacklist. +// This method validates the token format but cannot actually revoke it. +// For production use, consider implementing a token blacklist or use short-lived tokens. +func (s *STSService) RevokeSession(ctx context.Context, sessionToken string) error { if !s.initialized { return fmt.Errorf("STS service not initialized") } @@ -504,18 +483,16 @@ func (s *STSService) RevokeSession(ctx context.Context, filerAddress string, ses return fmt.Errorf("session token cannot be empty") } - // Use token generator for proper JWT validation - claims, err := s.tokenGenerator.ValidateSessionToken(sessionToken) + // Validate JWT token format + _, err := s.tokenGenerator.ValidateJWTWithClaims(sessionToken) if err != nil { return fmt.Errorf("invalid session token format: %w", err) } - // Remove session from store using session ID from claims - err = s.sessionStore.RevokeSession(ctx, filerAddress, claims.SessionId) - if err != nil { - return fmt.Errorf("failed to revoke session: %w", err) - } - + // In a stateless system, we cannot revoke JWT tokens without a blacklist + // The token will naturally expire based on its embedded expiration time + glog.V(1).Infof("Session revocation requested for stateless token - token will expire naturally at its embedded expiration time") + return nil } @@ -600,50 +577,19 @@ func (s *STSService) calculateSessionDuration(durationSeconds *int64) time.Durat return s.config.TokenDuration } -// extractSessionIdFromToken extracts session ID from session token +// extractSessionIdFromToken extracts session ID from JWT session token func (s *STSService) extractSessionIdFromToken(sessionToken string) string { - // For simplified implementation, we need to map session tokens to session IDs - // The session token is stored as part of the credentials in the session - // So we need to search through sessions to find the matching token - - // For now, use the session token directly as session ID since we store them together - // In a full implementation, this would parse JWT and extract session ID from claims - if len(sessionToken) > 10 && sessionToken[:2] == "ST" { - // Session token format - try to find the session by iterating - // This is inefficient but works for testing - return s.findSessionIdByToken(sessionToken) - } - - // For test compatibility, also handle direct session IDs - if len(sessionToken) == 32 { // Typical session ID length - return sessionToken - } - - return "" -} - -// findSessionIdByToken finds session ID by session token (simplified implementation) -func (s *STSService) findSessionIdByToken(sessionToken string) string { - // In a real implementation, we'd maintain a reverse index - // For testing, we can use the fact that our memory store can be searched - // This is a simplified approach - in production we'd use proper token->session mapping - - memStore, ok := s.sessionStore.(*MemorySessionStore) - if !ok { - return "" - } - - // Search through all sessions to find matching token - memStore.mutex.RLock() - defer memStore.mutex.RUnlock() - - for sessionId, session := range memStore.sessions { - if session.Credentials != nil && session.Credentials.SessionToken == sessionToken { - return sessionId + // Parse JWT and extract session ID from claims + claims, err := s.tokenGenerator.ValidateJWTWithClaims(sessionToken) + if err != nil { + // For test compatibility, also handle direct session IDs + if len(sessionToken) == 32 { // Typical session ID length + return sessionToken } + return "" } - return "" + return claims.SessionId } // validateAssumeRoleWithCredentialsRequest validates the credentials request parameters @@ -679,7 +625,7 @@ func (s *STSService) validateAssumeRoleWithCredentialsRequest(request *AssumeRol } // ExpireSessionForTesting manually expires a session for testing purposes -func (s *STSService) ExpireSessionForTesting(ctx context.Context, filerAddress string, sessionToken string) error { +func (s *STSService) ExpireSessionForTesting(ctx context.Context, sessionToken string) error { if !s.initialized { return fmt.Errorf("STS service not initialized") } @@ -688,18 +634,15 @@ func (s *STSService) ExpireSessionForTesting(ctx context.Context, filerAddress s return fmt.Errorf("session token cannot be empty") } - // Extract session ID from token - sessionId := s.extractSessionIdFromToken(sessionToken) - if sessionId == "" { - return fmt.Errorf("invalid session token format") - } - - // Check if session store supports manual expiration (for MemorySessionStore) - if memStore, ok := s.sessionStore.(*MemorySessionStore); ok { - return memStore.ExpireSessionForTesting(ctx, filerAddress, sessionId) + // Validate JWT token format + _, err := s.tokenGenerator.ValidateJWTWithClaims(sessionToken) + if err != nil { + return fmt.Errorf("invalid session token format: %w", err) } - // For other session stores, we could implement similar functionality - // For now, just return an error indicating it's not supported - return fmt.Errorf("manual session expiration not supported for this session store type") + // In a stateless system, we cannot manually expire JWT tokens + // The token expiration is embedded in the token itself and handled by JWT validation + glog.V(1).Infof("Manual session expiration requested for stateless token - cannot expire JWT tokens manually") + + return fmt.Errorf("manual session expiration not supported in stateless JWT system") } diff --git a/weed/iam/sts/sts_service_test.go b/weed/iam/sts/sts_service_test.go index 0959399ee..f4c61acc0 100644 --- a/weed/iam/sts/sts_service_test.go +++ b/weed/iam/sts/sts_service_test.go @@ -307,10 +307,11 @@ func setupTestSTSService(t *testing.T) *STSService { service := NewSTSService() config := &STSConfig{ - TokenDuration: time.Hour, - MaxSessionLength: time.Hour * 12, - Issuer: "test-sts", - SigningKey: []byte("test-signing-key-32-characters-long"), + TokenDuration: time.Hour, + MaxSessionLength: time.Hour * 12, + Issuer: "test-sts", + SigningKey: []byte("test-signing-key-32-characters-long"), + SessionStoreType: "memory", // Use memory store for unit tests } err := service.Initialize(config) diff --git a/weed/iam/sts/token_utils.go b/weed/iam/sts/token_utils.go index ae341ccef..12cddfb1c 100644 --- a/weed/iam/sts/token_utils.go +++ b/weed/iam/sts/token_utils.go @@ -25,14 +25,21 @@ func NewTokenGenerator(signingKey []byte, issuer string) *TokenGenerator { } } -// GenerateSessionToken creates a signed JWT session token +// GenerateSessionToken creates a signed JWT session token (legacy method for compatibility) func (t *TokenGenerator) GenerateSessionToken(sessionId string, expiresAt time.Time) (string, error) { - claims := jwt.MapClaims{ - JWTClaimIssuer: t.issuer, - JWTClaimSubject: sessionId, - JWTClaimIssuedAt: time.Now().Unix(), - JWTClaimExpiration: expiresAt.Unix(), - JWTClaimTokenType: TokenTypeSession, + claims := NewSTSSessionClaims(sessionId, t.issuer, expiresAt) + return t.GenerateJWTWithClaims(claims) +} + +// GenerateJWTWithClaims creates a signed JWT token with comprehensive session claims +func (t *TokenGenerator) GenerateJWTWithClaims(claims *STSSessionClaims) (string, error) { + if claims == nil { + return "", fmt.Errorf("claims cannot be nil") + } + + // Ensure issuer is set from token generator + if claims.Issuer == "" { + claims.Issuer = t.issuer } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -79,6 +86,46 @@ func (t *TokenGenerator) ValidateSessionToken(tokenString string) (*SessionToken }, nil } +// ValidateJWTWithClaims validates and extracts comprehensive session claims from a JWT token +func (t *TokenGenerator) ValidateJWTWithClaims(tokenString string) (*STSSessionClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &STSSessionClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return t.signingKey, nil + }) + + if err != nil { + return nil, fmt.Errorf(ErrInvalidToken, err) + } + + if !token.Valid { + return nil, fmt.Errorf(ErrTokenNotValid) + } + + claims, ok := token.Claims.(*STSSessionClaims) + if !ok { + return nil, fmt.Errorf(ErrInvalidTokenClaims) + } + + // Validate issuer + if claims.Issuer != t.issuer { + return nil, fmt.Errorf(ErrInvalidIssuer) + } + + // Validate that required fields are present + if claims.SessionId == "" { + return nil, fmt.Errorf(ErrMissingSessionID) + } + + // Additional validation using the claims' own validation method + if !claims.IsValid() { + return nil, fmt.Errorf(ErrTokenNotValid) + } + + return claims, nil +} + // SessionTokenClaims represents parsed session token claims type SessionTokenClaims struct { SessionId string diff --git a/weed/s3api/s3_iam_middleware.go b/weed/s3api/s3_iam_middleware.go index b74d825e6..2334e06f1 100644 --- a/weed/s3api/s3_iam_middleware.go +++ b/weed/s3api/s3_iam_middleware.go @@ -95,7 +95,7 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ } glog.V(0).Infof("AuthenticateJWT: calling IsActionAllowed for principal=%s", principalArn) - allowed, err := s3iam.iamManager.IsActionAllowed(ctx, s3iam.filerAddress, testRequest) + 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) @@ -147,7 +147,7 @@ func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IA } // Check if action is allowed using our policy engine - allowed, err := s3iam.iamManager.IsActionAllowed(ctx, s3iam.filerAddress, actionRequest) + allowed, err := s3iam.iamManager.IsActionAllowed(ctx, actionRequest) if err != nil { // Log the error but treat authentication/authorization failures as access denied // rather than internal errors to provide better user experience