package sts import ( "context" "encoding/json" "fmt" "strconv" "time" "github.com/golang-jwt/jwt/v5" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam/providers" "github.com/seaweedfs/seaweedfs/weed/iam/utils" ) // TrustPolicyValidator interface for validating trust policies during role assumption type TrustPolicyValidator interface { // ValidateTrustPolicyForWebIdentity validates if a web identity token can assume a role ValidateTrustPolicyForWebIdentity(ctx context.Context, roleArn string, webIdentityToken string) error // ValidateTrustPolicyForCredentials validates if credentials can assume a role ValidateTrustPolicyForCredentials(ctx context.Context, roleArn string, identity *providers.ExternalIdentity) error } // FlexibleDuration wraps time.Duration to support both integer nanoseconds and duration strings in JSON type FlexibleDuration struct { time.Duration } // UnmarshalJSON implements JSON unmarshaling for FlexibleDuration // Supports both: 3600000000000 (nanoseconds) and "1h" (duration string) func (fd *FlexibleDuration) UnmarshalJSON(data []byte) error { // Try to unmarshal as a duration string first (e.g., "1h", "30m") var durationStr string if err := json.Unmarshal(data, &durationStr); err == nil { duration, parseErr := time.ParseDuration(durationStr) if parseErr != nil { return fmt.Errorf("invalid duration string %q: %w", durationStr, parseErr) } fd.Duration = duration return nil } // If that fails, try to unmarshal as an integer (nanoseconds for backward compatibility) var nanoseconds int64 if err := json.Unmarshal(data, &nanoseconds); err == nil { fd.Duration = time.Duration(nanoseconds) return nil } // If both fail, try unmarshaling as a quoted number string (edge case) var numberStr string if err := json.Unmarshal(data, &numberStr); err == nil { if nanoseconds, parseErr := strconv.ParseInt(numberStr, 10, 64); parseErr == nil { fd.Duration = time.Duration(nanoseconds) return nil } } return fmt.Errorf("unable to parse duration from %s (expected duration string like \"1h\" or integer nanoseconds)", data) } // MarshalJSON implements JSON marshaling for FlexibleDuration // Always marshals as a human-readable duration string func (fd FlexibleDuration) MarshalJSON() ([]byte, error) { return json.Marshal(fd.Duration.String()) } // 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 // Public for access by other components initialized bool providers map[string]providers.IdentityProvider issuerToProvider map[string]providers.IdentityProvider // Efficient issuer-based provider lookup tokenGenerator *TokenGenerator trustPolicyValidator TrustPolicyValidator // Interface for trust policy validation } // STSConfig holds STS service configuration type STSConfig struct { // TokenDuration is the default duration for issued tokens TokenDuration FlexibleDuration `json:"tokenDuration"` // MaxSessionLength is the maximum duration for any session MaxSessionLength FlexibleDuration `json:"maxSessionLength"` // Issuer is the STS issuer identifier Issuer string `json:"issuer"` // SigningKey is used to sign session tokens SigningKey []byte `json:"signingKey"` // Providers configuration - enables automatic provider loading Providers []*ProviderConfig `json:"providers,omitempty"` } // ProviderConfig holds identity provider configuration type ProviderConfig struct { // Name is the unique identifier for the provider Name string `json:"name"` // Type specifies the provider type (oidc, ldap, etc.) Type string `json:"type"` // Config contains provider-specific configuration Config map[string]interface{} `json:"config"` // Enabled indicates if this provider should be active Enabled bool `json:"enabled"` } // AssumeRoleWithWebIdentityRequest represents a request to assume role with web identity type AssumeRoleWithWebIdentityRequest struct { // RoleArn is the ARN of the role to assume RoleArn string `json:"RoleArn"` // WebIdentityToken is the OIDC token from the identity provider WebIdentityToken string `json:"WebIdentityToken"` // RoleSessionName is a name for the assumed role session RoleSessionName string `json:"RoleSessionName"` // DurationSeconds is the duration of the role session (optional) DurationSeconds *int64 `json:"DurationSeconds,omitempty"` // Policy is an optional session policy (optional) Policy *string `json:"Policy,omitempty"` } // AssumeRoleWithCredentialsRequest represents a request to assume role with username/password type AssumeRoleWithCredentialsRequest struct { // RoleArn is the ARN of the role to assume RoleArn string `json:"RoleArn"` // Username is the username for authentication Username string `json:"Username"` // Password is the password for authentication Password string `json:"Password"` // RoleSessionName is a name for the assumed role session RoleSessionName string `json:"RoleSessionName"` // ProviderName is the name of the identity provider to use ProviderName string `json:"ProviderName"` // DurationSeconds is the duration of the role session (optional) DurationSeconds *int64 `json:"DurationSeconds,omitempty"` } // AssumeRoleResponse represents the response from assume role operations type AssumeRoleResponse struct { // Credentials contains the temporary security credentials Credentials *Credentials `json:"Credentials"` // AssumedRoleUser contains information about the assumed role user AssumedRoleUser *AssumedRoleUser `json:"AssumedRoleUser"` // PackedPolicySize is the percentage of max policy size used (AWS compatibility) PackedPolicySize *int64 `json:"PackedPolicySize,omitempty"` } // Credentials represents temporary security credentials type Credentials struct { // AccessKeyId is the access key ID AccessKeyId string `json:"AccessKeyId"` // SecretAccessKey is the secret access key SecretAccessKey string `json:"SecretAccessKey"` // SessionToken is the session token SessionToken string `json:"SessionToken"` // Expiration is when the credentials expire Expiration time.Time `json:"Expiration"` } // AssumedRoleUser contains information about the assumed role user type AssumedRoleUser struct { // AssumedRoleId is the unique identifier of the assumed role AssumedRoleId string `json:"AssumedRoleId"` // Arn is the ARN of the assumed role user Arn string `json:"Arn"` // Subject is the subject identifier from the identity provider Subject string `json:"Subject,omitempty"` } // SessionInfo represents information about an active session type SessionInfo struct { // SessionId is the unique identifier for the session SessionId string `json:"sessionId"` // SessionName is the name of the role session SessionName string `json:"sessionName"` // 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 (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"` // ExpiresAt is when the session expires ExpiresAt time.Time `json:"expiresAt"` // Credentials are the temporary credentials for this session Credentials *Credentials `json:"credentials"` } // NewSTSService creates a new STS service func NewSTSService() *STSService { return &STSService{ providers: make(map[string]providers.IdentityProvider), issuerToProvider: make(map[string]providers.IdentityProvider), } } // Initialize initializes the STS service with configuration func (s *STSService) Initialize(config *STSConfig) error { if config == nil { return fmt.Errorf(ErrConfigCannotBeNil) } if err := s.validateConfig(config); err != nil { return fmt.Errorf("invalid STS configuration: %w", err) } s.Config = config // Initialize token generator for stateless JWT operations s.tokenGenerator = NewTokenGenerator(config.SigningKey, config.Issuer) // Load identity providers from configuration if err := s.loadProvidersFromConfig(config); err != nil { return fmt.Errorf("failed to load identity providers: %w", err) } s.initialized = true return nil } // validateConfig validates the STS configuration func (s *STSService) validateConfig(config *STSConfig) error { if config.TokenDuration.Duration <= 0 { return fmt.Errorf(ErrInvalidTokenDuration) } if config.MaxSessionLength.Duration <= 0 { return fmt.Errorf(ErrInvalidMaxSessionLength) } if config.Issuer == "" { return fmt.Errorf(ErrIssuerRequired) } if len(config.SigningKey) < MinSigningKeyLength { return fmt.Errorf(ErrSigningKeyTooShort, MinSigningKeyLength) } return nil } // loadProvidersFromConfig loads identity providers from configuration func (s *STSService) loadProvidersFromConfig(config *STSConfig) error { if len(config.Providers) == 0 { glog.V(2).Infof("No providers configured in STS config") return nil } factory := NewProviderFactory() // Load all providers from configuration providersMap, err := factory.LoadProvidersFromConfig(config.Providers) if err != nil { return fmt.Errorf("failed to load providers from config: %w", err) } // Replace current providers with new ones s.providers = providersMap // Also populate the issuerToProvider map for efficient and secure JWT validation s.issuerToProvider = make(map[string]providers.IdentityProvider) for name, provider := range s.providers { issuer := s.extractIssuerFromProvider(provider) if issuer != "" { if _, exists := s.issuerToProvider[issuer]; exists { glog.Warningf("Duplicate issuer %s found for provider %s. Overwriting.", issuer, name) } s.issuerToProvider[issuer] = provider glog.V(2).Infof("Registered provider %s with issuer %s for efficient lookup", name, issuer) } } glog.V(1).Infof("Successfully loaded %d identity providers: %v", len(s.providers), s.getProviderNames()) return nil } // getProviderNames returns list of loaded provider names func (s *STSService) getProviderNames() []string { names := make([]string, 0, len(s.providers)) for name := range s.providers { names = append(names, name) } return names } // IsInitialized returns whether the service is initialized func (s *STSService) IsInitialized() bool { return s.initialized } // RegisterProvider registers an identity provider func (s *STSService) RegisterProvider(provider providers.IdentityProvider) error { if provider == nil { return fmt.Errorf(ErrProviderCannotBeNil) } name := provider.Name() if name == "" { return fmt.Errorf(ErrProviderNameEmpty) } s.providers[name] = provider // Try to extract issuer information for efficient lookup // This is a best-effort approach for different provider types issuer := s.extractIssuerFromProvider(provider) if issuer != "" { s.issuerToProvider[issuer] = provider glog.V(2).Infof("Registered provider %s with issuer %s for efficient lookup", name, issuer) } return nil } // extractIssuerFromProvider attempts to extract issuer information from different provider types func (s *STSService) extractIssuerFromProvider(provider providers.IdentityProvider) string { // Handle different provider types switch p := provider.(type) { case interface{ GetIssuer() string }: // For providers that implement GetIssuer() method return p.GetIssuer() default: // For other provider types, we'll rely on JWT parsing during validation // This is still more efficient than the current brute-force approach return "" } } // GetProviders returns all registered identity providers func (s *STSService) GetProviders() map[string]providers.IdentityProvider { return s.providers } // SetTrustPolicyValidator sets the trust policy validator for role assumption validation func (s *STSService) SetTrustPolicyValidator(validator TrustPolicyValidator) { s.trustPolicyValidator = validator } // AssumeRoleWithWebIdentity assumes a role using a web identity token (OIDC) // 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) } if request == nil { return nil, fmt.Errorf("request cannot be nil") } // Validate request parameters if err := s.validateAssumeRoleWithWebIdentityRequest(request); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Check for unsupported session policy if request.Policy != nil { return nil, fmt.Errorf("session policies are not currently supported - Policy parameter must be omitted") } // 1. Validate the web identity token with appropriate provider externalIdentity, provider, err := s.validateWebIdentityToken(ctx, request.WebIdentityToken) if err != nil { return nil, fmt.Errorf("failed to validate web identity token: %w", err) } // 2. Check if the role exists and can be assumed (includes trust policy validation) if err := s.validateRoleAssumptionForWebIdentity(ctx, request.RoleArn, request.WebIdentityToken); err != nil { return nil, fmt.Errorf("role assumption denied: %w", err) } // 3. Calculate session duration sessionDuration := s.calculateSessionDuration(request.DurationSeconds) expiresAt := time.Now().Add(sessionDuration) // 4. Generate session ID and credentials sessionId, err := GenerateSessionId() if err != nil { return nil, fmt.Errorf("failed to generate session ID: %w", err) } credGenerator := NewCredentialGenerator() credentials, err := credGenerator.GenerateTemporaryCredentials(sessionId, expiresAt) if err != nil { return nil, fmt.Errorf("failed to generate credentials: %w", err) } // 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). WithSessionName(request.RoleSessionName). 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 // 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") } if request == nil { return nil, fmt.Errorf("request cannot be nil") } // Validate request parameters if err := s.validateAssumeRoleWithCredentialsRequest(request); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // 1. Get the specified provider provider, exists := s.providers[request.ProviderName] if !exists { return nil, fmt.Errorf("identity provider not found: %s", request.ProviderName) } // 2. Validate credentials with the specified provider credentials := request.Username + ":" + request.Password externalIdentity, err := provider.Authenticate(ctx, credentials) if err != nil { return nil, fmt.Errorf("failed to authenticate credentials: %w", err) } // 3. Check if the role exists and can be assumed (includes trust policy validation) if err := s.validateRoleAssumptionForCredentials(ctx, request.RoleArn, externalIdentity); err != nil { return nil, fmt.Errorf("role assumption denied: %w", err) } // 4. Calculate session duration sessionDuration := s.calculateSessionDuration(request.DurationSeconds) expiresAt := time.Now().Add(sessionDuration) // 5. Generate session ID and temporary credentials sessionId, err := GenerateSessionId() if err != nil { return nil, fmt.Errorf("failed to generate session ID: %w", err) } credGenerator := NewCredentialGenerator() tempCredentials, err := credGenerator.GenerateTemporaryCredentials(sessionId, expiresAt) if err != nil { return nil, fmt.Errorf("failed to generate credentials: %w", err) } // 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). WithSessionName(request.RoleSessionName). 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, }, nil } // ValidateSessionToken validates a session token and returns session information // 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) } if sessionToken == "" { return nil, fmt.Errorf(ErrSessionTokenCannotBeEmpty) } // Validate JWT and extract comprehensive session claims claims, err := s.tokenGenerator.ValidateJWTWithClaims(sessionToken) if err != nil { return nil, fmt.Errorf(ErrSessionValidationFailed, err) } // Convert JWT claims back to SessionInfo // All session information is embedded in the JWT token itself return claims.ToSessionInfo(), nil } // NOTE: Session revocation is not supported in the stateless JWT design. // // In a stateless JWT system, tokens cannot be revoked without implementing a token blacklist, // which would break the stateless architecture. Tokens remain valid until their natural // expiration time. // // For applications requiring token revocation, consider: // 1. Using shorter token lifespans (e.g., 15-30 minutes) // 2. Implementing a distributed token blacklist (breaks stateless design) // 3. Including a "jti" (JWT ID) claim for tracking specific tokens // // Use ValidateSessionToken() to verify if a token is valid and not expired. // Helper methods for AssumeRoleWithWebIdentity // validateAssumeRoleWithWebIdentityRequest validates the request parameters func (s *STSService) validateAssumeRoleWithWebIdentityRequest(request *AssumeRoleWithWebIdentityRequest) error { if request.RoleArn == "" { return fmt.Errorf("RoleArn is required") } if request.WebIdentityToken == "" { return fmt.Errorf("WebIdentityToken is required") } if request.RoleSessionName == "" { return fmt.Errorf("RoleSessionName is required") } // Validate session duration if provided if request.DurationSeconds != nil { if *request.DurationSeconds < 900 || *request.DurationSeconds > 43200 { // 15min to 12 hours return fmt.Errorf("DurationSeconds must be between 900 and 43200 seconds") } } return nil } // validateWebIdentityToken validates the web identity token with strict issuer-to-provider mapping // SECURITY: JWT tokens with a specific issuer claim MUST only be validated by the provider for that issuer // SECURITY: This method only accepts JWT tokens. Non-JWT authentication must use AssumeRoleWithCredentials with explicit ProviderName. func (s *STSService) validateWebIdentityToken(ctx context.Context, token string) (*providers.ExternalIdentity, providers.IdentityProvider, error) { // Try to extract issuer from JWT token for strict validation issuer, err := s.extractIssuerFromJWT(token) if err != nil { // Token is not a valid JWT or cannot be parsed // SECURITY: Web identity tokens MUST be JWT tokens. Non-JWT authentication flows // should use AssumeRoleWithCredentials with explicit ProviderName to prevent // security vulnerabilities from non-deterministic provider selection. return nil, nil, fmt.Errorf("web identity token must be a valid JWT token: %w", err) } // Look up the specific provider for this issuer provider, exists := s.issuerToProvider[issuer] if !exists { // SECURITY: If no provider is registered for this issuer, fail immediately // This prevents JWT tokens from being validated by unintended providers return nil, nil, fmt.Errorf("no identity provider registered for issuer: %s", issuer) } // Authenticate with the correct provider for this issuer identity, err := provider.Authenticate(ctx, token) if err != nil { return nil, nil, fmt.Errorf("token validation failed with provider for issuer %s: %w", issuer, err) } if identity == nil { return nil, nil, fmt.Errorf("authentication succeeded but no identity returned for issuer %s", issuer) } return identity, provider, nil } // ValidateWebIdentityToken is a public method that exposes secure token validation for external use // This method uses issuer-based lookup to select the correct provider, ensuring security and efficiency func (s *STSService) ValidateWebIdentityToken(ctx context.Context, token string) (*providers.ExternalIdentity, providers.IdentityProvider, error) { return s.validateWebIdentityToken(ctx, token) } // extractIssuerFromJWT extracts the issuer (iss) claim from a JWT token without verification func (s *STSService) extractIssuerFromJWT(token string) (string, error) { // Parse token without verification to get claims parsedToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{}) if err != nil { return "", fmt.Errorf("failed to parse JWT token: %v", err) } // Extract claims claims, ok := parsedToken.Claims.(jwt.MapClaims) if !ok { return "", fmt.Errorf("invalid token claims") } // Get issuer claim issuer, ok := claims["iss"].(string) if !ok || issuer == "" { return "", fmt.Errorf("missing or invalid issuer claim") } return issuer, nil } // validateRoleAssumptionForWebIdentity validates role assumption for web identity tokens // This method performs complete trust policy validation to prevent unauthorized role assumptions func (s *STSService) validateRoleAssumptionForWebIdentity(ctx context.Context, roleArn string, webIdentityToken string) error { if roleArn == "" { return fmt.Errorf("role ARN cannot be empty") } if webIdentityToken == "" { return fmt.Errorf("web identity token cannot be empty") } // Basic role ARN format validation expectedPrefix := "arn:seaweed:iam::role/" if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix { return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix) } // Extract role name and validate ARN format roleName := utils.ExtractRoleNameFromArn(roleArn) if roleName == "" { return fmt.Errorf("invalid role ARN format: %s", roleArn) } // CRITICAL SECURITY: Perform trust policy validation if s.trustPolicyValidator != nil { if err := s.trustPolicyValidator.ValidateTrustPolicyForWebIdentity(ctx, roleArn, webIdentityToken); err != nil { return fmt.Errorf("trust policy validation failed: %w", err) } } else { // If no trust policy validator is configured, fail closed for security glog.Errorf("SECURITY WARNING: No trust policy validator configured - denying role assumption for security") return fmt.Errorf("trust policy validation not available - role assumption denied for security") } return nil } // validateRoleAssumptionForCredentials validates role assumption for credential-based authentication // This method performs complete trust policy validation to prevent unauthorized role assumptions func (s *STSService) validateRoleAssumptionForCredentials(ctx context.Context, roleArn string, identity *providers.ExternalIdentity) error { if roleArn == "" { return fmt.Errorf("role ARN cannot be empty") } if identity == nil { return fmt.Errorf("identity cannot be nil") } // Basic role ARN format validation expectedPrefix := "arn:seaweed:iam::role/" if len(roleArn) < len(expectedPrefix) || roleArn[:len(expectedPrefix)] != expectedPrefix { return fmt.Errorf("invalid role ARN format: got %s, expected format: %s*", roleArn, expectedPrefix) } // Extract role name and validate ARN format roleName := utils.ExtractRoleNameFromArn(roleArn) if roleName == "" { return fmt.Errorf("invalid role ARN format: %s", roleArn) } // CRITICAL SECURITY: Perform trust policy validation if s.trustPolicyValidator != nil { if err := s.trustPolicyValidator.ValidateTrustPolicyForCredentials(ctx, roleArn, identity); err != nil { return fmt.Errorf("trust policy validation failed: %w", err) } } else { // If no trust policy validator is configured, fail closed for security glog.Errorf("SECURITY WARNING: No trust policy validator configured - denying role assumption for security") return fmt.Errorf("trust policy validation not available - role assumption denied for security") } return nil } // calculateSessionDuration calculates the session duration func (s *STSService) calculateSessionDuration(durationSeconds *int64) time.Duration { if durationSeconds != nil { return time.Duration(*durationSeconds) * time.Second } // Use default from config return s.Config.TokenDuration.Duration } // extractSessionIdFromToken extracts session ID from JWT session token func (s *STSService) extractSessionIdFromToken(sessionToken string) string { // 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 claims.SessionId } // validateAssumeRoleWithCredentialsRequest validates the credentials request parameters func (s *STSService) validateAssumeRoleWithCredentialsRequest(request *AssumeRoleWithCredentialsRequest) error { if request.RoleArn == "" { return fmt.Errorf("RoleArn is required") } if request.Username == "" { return fmt.Errorf("Username is required") } if request.Password == "" { return fmt.Errorf("Password is required") } if request.RoleSessionName == "" { return fmt.Errorf("RoleSessionName is required") } if request.ProviderName == "" { return fmt.Errorf("ProviderName is required") } // Validate session duration if provided if request.DurationSeconds != nil { if *request.DurationSeconds < 900 || *request.DurationSeconds > 43200 { // 15min to 12 hours return fmt.Errorf("DurationSeconds must be between 900 and 43200 seconds") } } return nil } // ExpireSessionForTesting manually expires a session for testing purposes func (s *STSService) ExpireSessionForTesting(ctx context.Context, sessionToken string) error { if !s.initialized { return fmt.Errorf("STS service not initialized") } if sessionToken == "" { return fmt.Errorf("session token cannot be empty") } // Validate JWT token format _, err := s.tokenGenerator.ValidateJWTWithClaims(sessionToken) if err != nil { return fmt.Errorf("invalid session token format: %w", err) } // 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") }