| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- 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")
- }
|