You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
826 lines
29 KiB
826 lines
29 KiB
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")
|
|
}
|