diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-12-08 17:38:35 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-08 17:38:35 -0800 |
| commit | ff4855dcbe784eefa34e5f3298ebc071e10ed208 (patch) | |
| tree | 27fcc37d2e3b37c68146a02dc92cf623f14c27da /weed/iam/sts/sts_service.go | |
| parent | 772459f93ca5d77160c4b827a781a53ef91cc31c (diff) | |
| download | seaweedfs-ff4855dcbe784eefa34e5f3298ebc071e10ed208.tar.xz seaweedfs-ff4855dcbe784eefa34e5f3298ebc071e10ed208.zip | |
sts: limit session duration to incoming token's exp claim (#7670)
* sts: limit session duration to incoming token's exp claim
This fixes the issue where AssumeRoleWithWebIdentity would issue sessions
that outlive the source identity token's expiration.
For use cases like GitLab CI Jobs where the ID Token has an exp claim
limited to the CI job's timeout, the STS session should not exceed that
expiration.
Changes:
- Add TokenExpiration field to ExternalIdentity struct
- Extract exp/iat/nbf claims in OIDC provider's ValidateToken
- Pass token expiration from Authenticate to ExternalIdentity
- Modify calculateSessionDuration to cap at source token's exp
- Add comprehensive tests for the new behavior
Fixes: https://github.com/seaweedfs/seaweedfs/discussions/7653
* refactor: reduce duplication in time claim extraction
Use a loop over claim names instead of repeating the same
extraction logic three times for exp, iat, and nbf claims.
* address review: add defense-in-depth for expired tokens
- Handle already-expired tokens defensively with 1 minute minimum duration
- Enforce MaxSessionLength from config as additional cap
- Fix potential nil dereference in test mock
- Add test case for expired token scenario
* remove issue reference from test
* fix: remove early return to ensure MaxSessionLength is always checked
Diffstat (limited to 'weed/iam/sts/sts_service.go')
| -rw-r--r-- | weed/iam/sts/sts_service.go | 46 |
1 files changed, 38 insertions, 8 deletions
diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index 3d9f9af35..e28340f30 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -422,8 +422,9 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *Ass return nil, fmt.Errorf("role assumption denied: %w", err) } - // 3. Calculate session duration - sessionDuration := s.calculateSessionDuration(request.DurationSeconds) + // 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 @@ -502,7 +503,8 @@ func (s *STSService) AssumeRoleWithCredentials(ctx context.Context, request *Ass } // 4. Calculate session duration - sessionDuration := s.calculateSessionDuration(request.DurationSeconds) + // 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 @@ -745,14 +747,42 @@ func (s *STSService) validateRoleAssumptionForCredentials(ctx context.Context, r return nil } -// calculateSessionDuration calculates the session duration -func (s *STSService) calculateSessionDuration(durationSeconds *int64) time.Duration { +// 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 { - return time.Duration(*durationSeconds) * time.Second + 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 } - // Use default from config - return s.Config.TokenDuration.Duration + return duration } // extractSessionIdFromToken extracts session ID from JWT session token |
