* test: add integration tests for AssumeRole and AssumeRoleWithLDAPIdentity STS actions - Add s3_sts_assume_role_test.go with comprehensive tests for AssumeRole: * Parameter validation (missing RoleArn, RoleSessionName, invalid duration) * AWS SigV4 authentication with valid/invalid credentials * Temporary credential generation and usage - Add s3_sts_ldap_test.go with tests for AssumeRoleWithLDAPIdentity: * Parameter validation (missing LDAP credentials, RoleArn) * LDAP authentication scenarios (valid/invalid credentials) * Integration with LDAP server (when configured) - Update Makefile with new test targets: * test-sts: run all STS tests * test-sts-assume-role: run AssumeRole tests only * test-sts-ldap: run LDAP STS tests only * test-sts-suite: run tests with full service lifecycle - Enhance setup_all_tests.sh: * Add OpenLDAP container setup for LDAP testing * Create test LDAP users (testuser, ldapadmin) * Set LDAP environment variables for tests * Update cleanup to remove LDAP container - Fix setup_keycloak.sh: * Enable verbose error logging for realm creation * Improve error diagnostics Tests use fail-fast approach (t.Fatal) when server not configured, ensuring clear feedback when infrastructure is missing. * feat: implement AssumeRole and AssumeRoleWithLDAPIdentity STS actions Implement two new STS actions to match MinIO's STS feature set: **AssumeRole Implementation:** - Add handleAssumeRole with full AWS SigV4 authentication - Integrate with existing IAM infrastructure via verifyV4Signature - Validate required parameters (RoleArn, RoleSessionName) - Validate DurationSeconds (900-43200 seconds range) - Generate temporary credentials with expiration - Return AWS-compatible XML response **AssumeRoleWithLDAPIdentity Implementation:** - Add handleAssumeRoleWithLDAPIdentity handler (stub) - Validate LDAP-specific parameters (LDAPUsername, LDAPPassword) - Validate common STS parameters (RoleArn, RoleSessionName, DurationSeconds) - Return proper error messages for missing LDAP provider - Ready for LDAP provider integration **Routing Fixes:** - Add explicit routes for AssumeRole and AssumeRoleWithLDAPIdentity - Prevent IAM handler from intercepting authenticated STS requests - Ensure proper request routing priority **Handler Infrastructure:** - Add IAM field to STSHandlers for SigV4 verification - Update NewSTSHandlers to accept IAM reference - Add STS-specific error codes and response types - Implement writeSTSErrorResponse for AWS-compatible errors The AssumeRole action is fully functional and tested. AssumeRoleWithLDAPIdentity requires LDAP provider implementation. * fix: update IAM matcher to exclude STS actions from interception Update the IAM handler matcher to check for STS actions (AssumeRole, AssumeRoleWithWebIdentity, AssumeRoleWithLDAPIdentity) and exclude them from IAM handler processing. This allows STS requests to be handled by the STS fallback handler even when they include AWS SigV4 authentication. The matcher now parses the form data to check the Action parameter and returns false for STS actions, ensuring they are routed to the correct handler. Note: This is a work-in-progress fix. Tests are still showing some routing issues that need further investigation. * fix: address PR review security issues for STS handlers This commit addresses all critical security issues from PR review: Security Fixes: - Use crypto/rand for cryptographically secure credential generation instead of time.Now().UnixNano() (fixes predictable credentials) - Add sts:AssumeRole permission check via VerifyActionPermission to prevent unauthorized role assumption - Generate proper session tokens using crypto/rand instead of placeholder strings Code Quality Improvements: - Refactor DurationSeconds parsing into reusable parseDurationSeconds() helper function used by all three STS handlers - Create generateSecureCredentials() helper for consistent and secure temporary credential generation - Fix iamMatcher to check query string as fallback when Action not found in form data LDAP Provider Implementation: - Add go-ldap/ldap/v3 dependency - Create LDAPProvider implementing IdentityProvider interface with full LDAP authentication support (connect, bind, search, groups) - Update ProviderFactory to create real LDAP providers - Wire LDAP provider into AssumeRoleWithLDAPIdentity handler Test Infrastructure: - Add LDAP user creation verification step in setup_all_tests.sh * fix: address PR feedback (Round 2) - config validation & provider improvements - Implement `validateLDAPConfig` in `ProviderFactory` - Improve `LDAPProvider.Initialize`: - Support `connectionTimeout` parsing (string/int/float) from config map - Warn if `BindDN` is present but `BindPassword` is empty - Improve `LDAPProvider.GetUserInfo`: - Add fallback to `searchUserGroups` if `memberOf` returns no groups (consistent with Authenticate) * fix: address PR feedback (Round 3) - LDAP connection improvements & build fix - Improve `LDAPProvider` connection handling: - Use `net.Dialer` with configured timeout for connection establishment - Enforce TLS 1.2+ (`MinVersion: tls.VersionTLS12`) for both LDAPS and StartTLS - Fix build error in `s3api_sts.go` (format verb for ErrorCode) * fix: address PR feedback (Round 4) - LDAP hardening, Authz check & Routing fix - LDAP Provider Hardening: - Prevent re-initialization - Enforce single user match in `GetUserInfo` (was explicit only in Authenticate) - Ensure connection closure if StartTLS fails - STS Handlers: - Add robust provider detection using type assertion - **Security**: Implement authorization check (`VerifyActionPermission`) after LDAP authentication - Routing: - Update tests to reflect that STS actions are handled by STS handler, not generic IAM * fix: address PR feedback (Round 5) - JWT tokens, ARN formatting, PrincipalArn CRITICAL FIXES: - Replace standalone credential generation with STS service JWT tokens - handleAssumeRole now generates proper JWT session tokens - handleAssumeRoleWithLDAPIdentity now generates proper JWT session tokens - Session tokens can be validated across distributed instances - Fix ARN formatting in responses - Extract role name from ARN using utils.ExtractRoleNameFromArn() - Prevents malformed ARNs like "arn:aws:sts::assumed-role/arn:aws:iam::..." - Add configurable AccountId for federated users - Add AccountId field to STSConfig (defaults to "111122223333") - PrincipalArn now uses configured account ID instead of hardcoded "aws" - Enables proper trust policy validation IMPROVEMENTS: - Sanitize LDAP authentication error messages (don't leak internal details) - Remove duplicate comment in provider detection - Add utils import for ARN parsing utilities * feat: implement LDAP connection pooling to prevent resource exhaustion PERFORMANCE IMPROVEMENT: - Add connection pool to LDAPProvider (default size: 10 connections) - Reuse LDAP connections across authentication requests - Prevent file descriptor exhaustion under high load IMPLEMENTATION: - connectionPool struct with channel-based connection management - getConnection(): retrieves from pool or creates new connection - returnConnection(): returns healthy connections to pool - createConnection(): establishes new LDAP connection with TLS support - Close(): cleanup method to close all pooled connections - Connection health checking (IsClosing()) before reuse BENEFITS: - Reduced connection overhead (no TCP handshake per request) - Better resource utilization under load - Prevents "too many open files" errors - Non-blocking pool operations (creates new conn if pool empty) * fix: correct TokenGenerator access in STS handlers CRITICAL FIX: - Make TokenGenerator public in STSService (was private tokenGenerator) - Update all references from Config.TokenGenerator to TokenGenerator - Remove TokenGenerator from STSConfig (it belongs in STSService) This fixes the "NotImplemented" errors in distributed and Keycloak tests. The issue was that Round 5 changes tried to access Config.TokenGenerator which didn't exist - TokenGenerator is a field in STSService, not STSConfig. The TokenGenerator is properly initialized in STSService.Initialize() and is now accessible for JWT token generation in AssumeRole handlers. * fix: update tests to use public TokenGenerator field Following the change to make TokenGenerator public in STSService, this commit updates the test files to reference the correct public field name. This resolves compilation errors in the IAM STS test suite. * fix: update distributed tests to use valid Keycloak users Updated s3_iam_distributed_test.go to use 'admin-user' and 'read-user' which exist in the standard Keycloak setup provided by setup_keycloak.sh. This resolves 'unknown test user' errors in distributed integration tests. * fix: ensure iam_config.json exists in setup target for CI The GitHub Actions workflow calls 'make setup' which was not creating iam_config.json, causing the server to start without IAM integration enabled (iamIntegration = nil), resulting in NotImplemented errors. Now 'make setup' copies iam_config.local.json to iam_config.json if it doesn't exist, ensuring IAM is properly configured in CI. * fix(iam/ldap): fix connection pool race and rebind corruption - Add atomic 'closed' flag to connection pool to prevent racing on Close() - Rebind authenticated user connections back to service account before returning to pool - Close connections on error instead of returning potentially corrupted state to pool * fix(iam/ldap): populate standard TokenClaims fields in ValidateToken - Set Subject, Issuer, Audience, IssuedAt, and ExpiresAt to satisfy the interface - Use time.Time for timestamps as required by TokenClaims struct - Default to 1 hour TTL for LDAP tokens * fix(s3api): include account ID in STS AssumedRoleUser ARN - Consistent with AWS, include the account ID in the assumed-role ARN - Use the configured account ID from STS service if available, otherwise default to '111122223333' - Apply to both AssumeRole and AssumeRoleWithLDAPIdentity handlers - Also update .gitignore to ignore IAM test environment files * refactor(s3api): extract shared STS credential generation logic - Move common logic for session claims and credential generation to prepareSTSCredentials - Update handleAssumeRole and handleAssumeRoleWithLDAPIdentity to use the helper - Remove stale comments referencing outdated line numbers * feat(iam/ldap): make pool size configurable and add audience support - Add PoolSize to LDAPConfig (default 10) - Add Audience to LDAPConfig to align with OIDC validation - Update initialization and ValidateToken to use new fields * update tests * debug * chore(iam): cleanup debug prints and fix test config port * refactor(iam): use mapstructure for LDAP config parsing * feat(sts): implement strict trust policy validation for AssumeRole * test(iam): refactor STS tests to use AWS SDK signer * test(s3api): implement ValidateTrustPolicyForPrincipal in MockIAMIntegration * fix(s3api): ensure IAM matcher checks query string on ParseForm error * fix(sts): use crypto/rand for secure credentials and extract constants * fix(iam): fix ldap connection leaks and add insecure warning * chore(iam): improved error wrapping and test parameterization * feat(sts): add support for LDAPProviderName parameter * Update weed/iam/ldap/ldap_provider.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/s3api/s3api_sts.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(sts): use STSErrSTSNotReady when LDAP provider is missing * fix(sts): encapsulate TokenGenerator in STSService and add getter --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
887 lines
32 KiB
Go
887 lines
32 KiB
Go
package sts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"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
|
|
// durationSeconds is optional and can be nil
|
|
ValidateTrustPolicyForWebIdentity(ctx context.Context, roleArn string, webIdentityToken string, durationSeconds *int64) 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
|
|
}
|
|
|
|
// GetTokenGenerator returns the token generator used by the STS service.
|
|
// This keeps the underlying field unexported while still allowing read-only access.
|
|
func (s *STSService) GetTokenGenerator() *TokenGenerator {
|
|
return s.tokenGenerator
|
|
}
|
|
|
|
// 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"`
|
|
|
|
// AccountId is the AWS account ID used for federated user ARNs
|
|
// Defaults to "111122223333" if not specified
|
|
AccountId string `json:"accountId,omitempty"`
|
|
|
|
// 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, request.DurationSeconds); err != nil {
|
|
return nil, fmt.Errorf("role assumption denied: %w", err)
|
|
}
|
|
|
|
// 3. Calculate session duration, capping at the source token's expiration
|
|
// This ensures sessions from short-lived tokens (e.g., GitLab CI job tokens) don't outlive their source
|
|
sessionDuration := s.calculateSessionDuration(request.DurationSeconds, externalIdentity.TokenExpiration)
|
|
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
|
|
// For credential-based auth, there's no source token with expiration to cap against
|
|
sessionDuration := s.calculateSessionDuration(request.DurationSeconds, nil)
|
|
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 {
|
|
// Map provider errors to STS errors using errors.Is() for robust error checking
|
|
// This eliminates fragile string matching and provides reliable error classification
|
|
if errors.Is(err, providers.ErrProviderTokenExpired) {
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTypedTokenExpired, err)
|
|
} else if errors.Is(err, providers.ErrProviderInvalidToken) {
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTypedInvalidToken, err)
|
|
} else if errors.Is(err, providers.ErrProviderInvalidIssuer) {
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTypedInvalidIssuer, err)
|
|
} else if errors.Is(err, providers.ErrProviderInvalidAudience) {
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTypedInvalidAudience, err)
|
|
} else if errors.Is(err, providers.ErrProviderMissingClaims) {
|
|
return nil, nil, fmt.Errorf("%w: %v", ErrTypedMissingClaims, err)
|
|
}
|
|
// For other errors, return with context
|
|
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, durationSeconds *int64) error {
|
|
if roleArn == "" {
|
|
return fmt.Errorf("role ARN cannot be empty")
|
|
}
|
|
|
|
if webIdentityToken == "" {
|
|
return fmt.Errorf("web identity token cannot be empty")
|
|
}
|
|
|
|
// Validate role ARN and extract role information
|
|
// Accepts both arn:aws:iam::role/X and arn:aws:iam::ACCOUNT:role/X
|
|
arnInfo := utils.ParseRoleARN(roleArn)
|
|
if arnInfo.RoleName == "" {
|
|
return fmt.Errorf("invalid role ARN format: %s, expected format: arn:aws:iam::[ACCOUNT_ID:]role/ROLE_NAME", roleArn)
|
|
}
|
|
|
|
// Log ARN details for debugging
|
|
if arnInfo.AccountID != "" {
|
|
glog.V(4).Infof("Role ARN validation: role=%s, account=%s (standard format)", arnInfo.RoleName, arnInfo.AccountID)
|
|
} else {
|
|
glog.V(4).Infof("Role ARN validation: role=%s (legacy format)", arnInfo.RoleName)
|
|
}
|
|
|
|
// CRITICAL SECURITY: Perform trust policy validation
|
|
if s.trustPolicyValidator != nil {
|
|
if err := s.trustPolicyValidator.ValidateTrustPolicyForWebIdentity(ctx, roleArn, webIdentityToken, durationSeconds); 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")
|
|
}
|
|
|
|
// Validate role ARN and extract role information
|
|
// Accepts both arn:aws:iam::role/X and arn:aws:iam::ACCOUNT:role/X
|
|
arnInfo := utils.ParseRoleARN(roleArn)
|
|
if arnInfo.RoleName == "" {
|
|
return fmt.Errorf("invalid role ARN format: %s, expected format: arn:aws:iam::[ACCOUNT_ID:]role/ROLE_NAME", roleArn)
|
|
}
|
|
|
|
// Log ARN details for debugging
|
|
if arnInfo.AccountID != "" {
|
|
glog.V(4).Infof("Role ARN validation: role=%s, account=%s (standard format)", arnInfo.RoleName, arnInfo.AccountID)
|
|
} else {
|
|
glog.V(4).Infof("Role ARN validation: role=%s (legacy format)", arnInfo.RoleName)
|
|
}
|
|
|
|
// 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, respecting the source token's expiration
|
|
// If the incoming web identity token has an exp claim, the session duration is capped to not exceed it
|
|
// This ensures that sessions from short-lived tokens (e.g., GitLab CI job tokens) don't outlive their source
|
|
func (s *STSService) calculateSessionDuration(durationSeconds *int64, tokenExpiration *time.Time) time.Duration {
|
|
var duration time.Duration
|
|
if durationSeconds != nil {
|
|
duration = time.Duration(*durationSeconds) * time.Second
|
|
} else {
|
|
// Use default from config
|
|
duration = s.Config.TokenDuration.Duration
|
|
}
|
|
|
|
// If the source token has an expiration, cap the session duration to not exceed it
|
|
// This follows the principle: "if calculated exp > incoming exp claim, then limit outgoing exp to incoming exp"
|
|
if tokenExpiration != nil && !tokenExpiration.IsZero() {
|
|
timeUntilTokenExpiry := time.Until(*tokenExpiration)
|
|
if timeUntilTokenExpiry <= 0 {
|
|
// Token already expired - use minimal duration as defense-in-depth
|
|
// The token should have been rejected during validation, but we handle this defensively
|
|
glog.V(2).Infof("Source token already expired, using minimal session duration")
|
|
duration = time.Minute
|
|
} else if timeUntilTokenExpiry < duration {
|
|
glog.V(2).Infof("Limiting session duration from %v to %v based on source token expiration",
|
|
duration, timeUntilTokenExpiry)
|
|
duration = timeUntilTokenExpiry
|
|
}
|
|
}
|
|
|
|
// Cap at MaxSessionLength if configured
|
|
if s.Config.MaxSessionLength.Duration > 0 && duration > s.Config.MaxSessionLength.Duration {
|
|
glog.V(2).Infof("Limiting session duration from %v to %v based on MaxSessionLength config",
|
|
duration, s.Config.MaxSessionLength.Duration)
|
|
duration = s.Config.MaxSessionLength.Duration
|
|
}
|
|
|
|
return duration
|
|
}
|
|
|
|
// extractSessionIdFromToken extracts session ID from JWT session token
|
|
func (s *STSService) extractSessionIdFromToken(sessionToken string) string {
|
|
// Validate JWT and extract session 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")
|
|
}
|
|
|
|
// Just validate the signature
|
|
_, 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")
|
|
}
|