diff --git a/extensions/test/CrtIntegrationTests/V4aSignerTests.cs b/extensions/test/CrtIntegrationTests/V4aSignerTests.cs index 88a68305b7aa..90cdd352bc7b 100644 --- a/extensions/test/CrtIntegrationTests/V4aSignerTests.cs +++ b/extensions/test/CrtIntegrationTests/V4aSignerTests.cs @@ -440,5 +440,96 @@ public void TestChunkedRequestWithTrailingHeaders() Encoding.ASCII.GetBytes(trailerChunkResult), SigningTestEccPubX, SigningTestEccPubY)); } #endregion + + #region Multi-Region SigV4a Signing Tests + + /// + /// Tests multi-region SigV4a signing with different region set configurations. + /// + /// The CRT library handles the x-amz-region-set header internally during the signing process. + /// This test verifies that different region configurations produce different signatures + /// and that the region set is actually used in the signing calculation. + /// + [Fact] + public void TestMultiRegionSigV4a_DifferentRegionSetsProduceDifferentSignatures() + { + var signer = new CrtAWS4aSigner(); + var clientConfig = BuildSigningClientConfig(SigningTestService); + + // Test with different region configurations + var regionSets = new[] { "us-west-2", "us-west-2,us-east-1", "*", "eu-west-1" }; + var signatures = new Dictionary(); + + foreach (var regionSet in regionSets) + { + var request = BuildHeaderRequestToSign("/", new Dictionary()); + // AuthenticationRegion now handles both SigV4 and SigV4a regions + request.AuthenticationRegion = regionSet; + + var result = signer.SignRequest(request, clientConfig, null, SigningTestCredentials); + + // Verify basic result properties + Assert.NotNull(result); + Assert.NotNull(result.Signature); + Assert.NotEmpty(result.Signature); + Assert.Equal(regionSet, result.RegionSet); + + // Store signature for comparison + signatures[regionSet] = result.Signature; + } + + // Verify that different region sets produce different signatures + // This ensures the region set is actually being used in the signing calculation + Assert.NotEqual(signatures["us-west-2"], signatures["us-west-2,us-east-1"]); + Assert.NotEqual(signatures["us-west-2"], signatures["*"]); + Assert.NotEqual(signatures["us-west-2"], signatures["eu-west-1"]); + Assert.NotEqual(signatures["us-west-2,us-east-1"], signatures["*"]); + + // The x-amz-region-set header is handled internally by CRT for signing. + // Different signatures confirm that multi-region information is being used correctly. + } + + /// + /// Tests backward compatibility: when AuthenticationRegion is not set, + /// the signer should fall back to single-region behavior. + /// + [Fact] + public void TestSigV4aFallbackToSingleRegion() + { + var signer = new CrtAWS4aSigner(); + var request = BuildHeaderRequestToSign("/", new Dictionary()); + // Explicitly NOT setting request.AuthenticationRegion for multi-region + + var clientConfig = BuildSigningClientConfig(SigningTestService); + var result = signer.SignRequest(request, clientConfig, null, SigningTestCredentials); + + // Should fall back to us-east-1 (from SigningTestRegion) + Assert.Equal(SigningTestRegion, request.DeterminedSigningRegion); + Assert.True(request.Headers.ContainsKey(HeaderKeys.XAmzRegionSetHeader)); + Assert.Equal(SigningTestRegion, request.Headers[HeaderKeys.XAmzRegionSetHeader]); + + var expectedCanonicalRequest = String.Join('\n', + "POST", "/", "", + "content-length:13", + "content-type:application/x-www-form-urlencoded", + "host:example.amazonaws.com", + "x-amz-content-sha256:9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e", + "x-amz-date:20150830T123600Z", + $"x-amz-region-set:{SigningTestRegion}", + "", + "content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-region-set", + "9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e"); + + var config = BuildDefaultSigningConfig(SigningTestService); + config.SignatureType = AwsSignatureType.CANONICAL_REQUEST_VIA_HEADERS; + config.Region = SigningTestRegion; + + Assert.True(AwsSigner.VerifyV4aCanonicalSigning( + expectedCanonicalRequest, config, result.Signature, + SigningTestEccPubX, SigningTestEccPubY), + "Fallback signature verification failed"); + } + + #endregion } } diff --git a/generator/.DevConfigs/3aa6313d-9526-40ba-b09c-e046e0d4ef2f.json b/generator/.DevConfigs/3aa6313d-9526-40ba-b09c-e046e0d4ef2f.json new file mode 100644 index 000000000000..d8f0fc085bb8 --- /dev/null +++ b/generator/.DevConfigs/3aa6313d-9526-40ba-b09c-e046e0d4ef2f.json @@ -0,0 +1,16 @@ +{ + "core": { + "changeLogMessages": [ + "Added ability to configure authentication scheme preferences (e.g., prioritize SigV4a over SigV4)", + "Added support for AWS_AUTH_SCHEME_PREFERENCE environment variable and auth_scheme_preference configuration file setting", + "Added support for AWS_SIGV4A_SIGNING_REGION_SET environment variable and sigv4a_signing_region_set profile key to configure SigV4a signing region set" + ], + "type": "minor", + "updateMinimum": true, + "backwardIncompatibilitiesToIgnore": [ + "Amazon.Runtime.Internal.IRequest/MethodAbstractMethodAdded", + "Amazon.Runtime.IClientConfig/MethodAbstractMethodAdded", + "Amazon.Runtime.IRequestContext/MethodAbstractMethodAdded" + ] + } +} \ No newline at end of file diff --git a/sdk/src/Core/Amazon.Runtime/ClientConfig.cs b/sdk/src/Core/Amazon.Runtime/ClientConfig.cs index d6753b4c5f99..d24e8fccfe8e 100644 --- a/sdk/src/Core/Amazon.Runtime/ClientConfig.cs +++ b/sdk/src/Core/Amazon.Runtime/ClientConfig.cs @@ -14,6 +14,7 @@ */ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading; using System.Globalization; @@ -66,6 +67,10 @@ public abstract partial class ClientConfig : IClientConfig private string serviceURL = null; private string authRegion = null; private string authServiceName = null; + private string authSchemePreference = null; + private string sigV4aSigningRegionSet = null; + private List _authSchemePreferenceList = null; + private List _sigV4aSigningRegionSetList = null; private string clientAppId = null; private SigningAlgorithm signatureMethod = SigningAlgorithm.HmacSHA256; private bool logResponse = false; @@ -444,6 +449,106 @@ public string AuthenticationServiceName get { return this.authServiceName; } set { this.authServiceName = value; } } + + /// + /// Gets and sets the AuthSchemePreference property. + /// A comma-separated list of authentication scheme names to use in order of preference. + /// + public string AuthSchemePreference + { + get + { + // Return cached string if explicitly set + if (!string.IsNullOrEmpty(this.authSchemePreference)) + return this.authSchemePreference; + + // Fallback to config. Convert List to string for backward compatibility + var fallback = FallbackInternalConfigurationFactory.AuthSchemePreference; + return fallback != null ? string.Join(",", fallback) : null; + } + set + { + this.authSchemePreference = value; + + if (string.IsNullOrEmpty(value)) + { + this._authSchemePreferenceList = null; + } + else + { + this._authSchemePreferenceList = value.Split(',') + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .Distinct() + .ToList(); + } + } + } + + /// + /// Internal accessor, uses pre-parsed list + /// + internal List AuthSchemePreferenceList + { + get + { + if (this._authSchemePreferenceList != null) + return this._authSchemePreferenceList; + + return FallbackInternalConfigurationFactory.AuthSchemePreference; + } + } + + /// + /// Gets and sets the SigV4aSigningRegionSet property. + /// A comma-separated list of regions that a SigV4a signature will be valid for. + /// Use "*" to indicate all regions. + /// + public string SigV4aSigningRegionSet + { + get + { + // Return cached string if explicitly set + if (!string.IsNullOrEmpty(this.sigV4aSigningRegionSet)) + return this.sigV4aSigningRegionSet; + + // Fallback to config + var fallback = FallbackInternalConfigurationFactory.SigV4aSigningRegionSet; + return fallback != null ? string.Join(",", fallback) : null; + } + set + { + this.sigV4aSigningRegionSet = value; + + // Parse immediately and cache as List + if (string.IsNullOrEmpty(value)) + { + this._sigV4aSigningRegionSetList = null; + } + else + { + this._sigV4aSigningRegionSetList = value.Split(',') + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .Distinct() // Deduplicate + .ToList(); + } + } + } + + /// + /// Internal accessor, uses pre-parsed list + /// + internal List SigV4aSigningRegionSetList + { + get + { + if (this._sigV4aSigningRegionSetList != null) + return this._sigV4aSigningRegionSetList; + + return FallbackInternalConfigurationFactory.SigV4aSigningRegionSet; + } + } /// /// The serviceId for the service, which is specified in the metadata in the ServiceModel. diff --git a/sdk/src/Core/Amazon.Runtime/CredentialManagement/CredentialProfile.cs b/sdk/src/Core/Amazon.Runtime/CredentialManagement/CredentialProfile.cs index f0450d5d0a69..d1eb0e4f6865 100644 --- a/sdk/src/Core/Amazon.Runtime/CredentialManagement/CredentialProfile.cs +++ b/sdk/src/Core/Amazon.Runtime/CredentialManagement/CredentialProfile.cs @@ -192,6 +192,18 @@ internal Dictionary> NestedProperties /// public AccountIdEndpointMode? AccountIdEndpointMode { get; set; } + /// + /// Preference list of authentication schemes to use when multiple schemes are available. + /// Short names without namespace (e.g., "sigv4" not "aws.auth#sigv4") + /// + public List AuthSchemePreference { get; set; } + + /// + /// The region set to use for SigV4a signing. This can be a single region, + /// a list of regions, or "*" for all regions. + /// + public List SigV4aSigningRegionSet { get; set; } + /// /// An optional dictionary of name-value pairs stored with the CredentialProfile /// diff --git a/sdk/src/Core/Amazon.Runtime/CredentialManagement/SharedCredentialsFile.cs b/sdk/src/Core/Amazon.Runtime/CredentialManagement/SharedCredentialsFile.cs index bb4f039394c6..30a27445202e 100644 --- a/sdk/src/Core/Amazon.Runtime/CredentialManagement/SharedCredentialsFile.cs +++ b/sdk/src/Core/Amazon.Runtime/CredentialManagement/SharedCredentialsFile.cs @@ -71,6 +71,8 @@ public class SharedCredentialsFile : ICredentialProfileStore private const string AccountIdEndpointModeField = "account_id_endpoint_mode"; private const string RequestChecksumCalculationField = "request_checksum_calculation"; private const string ResponseChecksumValidationField = "response_checksum_validation"; + private const string AuthSchemePreferenceField = "auth_scheme_preference"; + private const string SigV4aSigningRegionSetField = "sigv4a_signing_region_set"; private const string AwsAccountIdField = "aws_account_id"; private readonly Logger _logger = Logger.GetLogger(typeof(SharedCredentialsFile)); @@ -106,6 +108,8 @@ public class SharedCredentialsFile : ICredentialProfileStore AccountIdEndpointModeField, RequestChecksumCalculationField, ResponseChecksumValidationField, + AuthSchemePreferenceField, + SigV4aSigningRegionSetField, AwsAccountIdField, }; @@ -405,6 +409,12 @@ private void RegisterProfileInternal(CredentialProfile profile) if (profile.Services != null) reservedProperties[ServicesField] = profile.Services.ToString().ToLowerInvariant(); + if (profile.AuthSchemePreference != null && profile.AuthSchemePreference.Count > 0) + reservedProperties[AuthSchemePreferenceField] = string.Join(",", profile.AuthSchemePreference); + + if (profile.SigV4aSigningRegionSet != null && profile.SigV4aSigningRegionSet.Count > 0) + reservedProperties[SigV4aSigningRegionSetField] = string.Join(",", profile.SigV4aSigningRegionSet); + var profileDictionary = PropertyMapping.CombineProfileParts( profile.Options, ReservedPropertyNames, reservedProperties, profile.Properties); @@ -859,6 +869,19 @@ private bool TryGetProfile(string profileName, bool doRefresh, bool isSsoSession } responseChecksumValidation = responseChecksumValidationTemp; } + + List authSchemePreference = null; + if (reservedProperties.TryGetValue(AuthSchemePreferenceField, out var authSchemePrefString)) + { + authSchemePreference = ParseCommaDelimitedList(authSchemePrefString); + } + + List sigV4aSigningRegionSet = null; + if (reservedProperties.TryGetValue(SigV4aSigningRegionSetField, out var sigV4aRegionSetString)) + { + sigV4aSigningRegionSet = ParseCommaDelimitedList(sigV4aRegionSetString); + } + profile = new CredentialProfile(profileName, profileOptions) { UniqueKey = toolkitArtifactGuid, @@ -886,6 +909,8 @@ private bool TryGetProfile(string profileName, bool doRefresh, bool isSsoSession AccountIdEndpointMode = accountIdEndpointMode, RequestChecksumCalculation = requestChecksumCalculation, ResponseChecksumValidation = responseChecksumValidation, + AuthSchemePreference = authSchemePreference, + SigV4aSigningRegionSet = sigV4aSigningRegionSet, Services = servicesSection }; @@ -943,6 +968,20 @@ private bool TryGetSection(string sectionName, bool isSsoSession, bool isService return hasCredentialsProperties; } + /// + /// Parses comma-delimited list from profile file. + /// + private static List ParseCommaDelimitedList(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + return value.Split(',') + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + } + private static bool IsSupportedProfileType(CredentialProfileType? profileType) { return !profileType.HasValue || ProfileTypeWhitelist.Contains(profileType.Value); diff --git a/sdk/src/Core/Amazon.Runtime/IClientConfig.cs b/sdk/src/Core/Amazon.Runtime/IClientConfig.cs index 089476e1c673..6878ee3fde36 100644 --- a/sdk/src/Core/Amazon.Runtime/IClientConfig.cs +++ b/sdk/src/Core/Amazon.Runtime/IClientConfig.cs @@ -163,6 +163,20 @@ public partial interface IClientConfig /// string AuthenticationServiceName { get; } + /// + /// Gets the AuthSchemePreference property. + /// A comma-separated list of authentication scheme names to use in order of preference. + /// For example: "sigv4a,sigv4" to prefer SigV4a over SigV4. + /// + string AuthSchemePreference { get; } + + /// + /// Gets the SigV4aSigningRegionSet property. + /// A comma-separated list of regions that a SigV4a signature will be valid for. + /// Use "*" to indicate all regions. + /// + string SigV4aSigningRegionSet { get; } + /// /// Gets the UserAgent property. /// diff --git a/sdk/src/Core/Amazon.Runtime/Internal/DefaultRequest.cs b/sdk/src/Core/Amazon.Runtime/Internal/DefaultRequest.cs index bce0a81fe37f..087052fb0f3c 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/DefaultRequest.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/DefaultRequest.cs @@ -471,7 +471,9 @@ public string CanonicalResourcePrefix /// /// The authentication region to use for the request. - /// Set from Config.AuthenticationRegion. + /// For SigV4: Contains a single region (e.g., "us-west-2"). + /// For SigV4a: Contains a comma-separated list of regions (e.g., "us-west-2,us-east-1") or "*" for all regions. + /// Set from Config.AuthenticationRegion or Config.SigV4aSigningRegionSet. /// public string AuthenticationRegion { get; set; } diff --git a/sdk/src/Core/Amazon.Runtime/Internal/IRequest.cs b/sdk/src/Core/Amazon.Runtime/Internal/IRequest.cs index 6f3d44cf711d..f921580d02d7 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/IRequest.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/IRequest.cs @@ -333,12 +333,17 @@ string CanonicalResourcePrefix /// /// The authentication region to use for the request. - /// Set from Config.AuthenticationRegion. + /// For SigV4: Contains a single region (e.g., "us-west-2"). + /// For SigV4a: Contains a comma-separated list of regions (e.g., "us-west-2,us-east-1") or "*" for all regions. + /// Set from Config.AuthenticationRegion or Config.SigV4aSigningRegionSet. /// string AuthenticationRegion { get; set; } /// - /// The region in which the service request was signed. + /// The region or region set used for signing the service request. + /// For standard SigV4 signing, this contains a single region (e.g., "us-west-2"). + /// For SigV4a multi-region signing, this can be a comma-separated list of regions (e.g., "us-west-2,us-east-1") + /// or "*" to indicate the signature is valid for all regions. /// string DeterminedSigningRegion { get; set; } diff --git a/sdk/src/Core/Amazon.Runtime/Internal/InternalConfiguration.cs b/sdk/src/Core/Amazon.Runtime/Internal/InternalConfiguration.cs index aa6b95ce8770..969944409336 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/InternalConfiguration.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/InternalConfiguration.cs @@ -114,6 +114,17 @@ public class InternalConfiguration /// Determines the behavior for validating checksums on response payloads. /// public ResponseChecksumValidation? ResponseChecksumValidation { get; set; } + + /// + /// Preference list of authentication schemes to use when multiple schemes are available. + /// Parsed from comma-separated format at load time. + /// + public List AuthSchemePreference { get; set; } + + /// + /// The region set to use for SigV4a signing. + /// + public List SigV4aSigningRegionSet { get; set; } } #if BCL || NETSTANDARD @@ -140,6 +151,8 @@ public class EnvironmentVariableInternalConfiguration : InternalConfiguration public const string ENVIRONMENT_VARAIBLE_AWS_ACCOUNT_ID_ENDPOINT_MODE = "AWS_ACCOUNT_ID_ENDPOINT_MODE"; public const string ENVIRONMENT_VARIABLE_AWS_REQUEST_CHECKSUM_CALCULATION = "AWS_REQUEST_CHECKSUM_CALCULATION"; public const string ENVIRONMENT_VARIABLE_AWS_RESPONSE_CHECKSUM_VALIDATION = "AWS_RESPONSE_CHECKSUM_VALIDATION"; + public const string ENVIRONMENT_VARIABLE_AWS_AUTH_SCHEME_PREFERENCE = "AWS_AUTH_SCHEME_PREFERENCE"; + public const string ENVIRONMENT_VARIABLE_AWS_SIGV4A_SIGNING_REGION_SET = "AWS_SIGV4A_SIGNING_REGION_SET"; public const int AWS_SDK_UA_APP_ID_MAX_LENGTH = 50; /// @@ -165,6 +178,8 @@ public EnvironmentVariableInternalConfiguration() RequestChecksumCalculation = GetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_REQUEST_CHECKSUM_CALCULATION); ResponseChecksumValidation = GetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_RESPONSE_CHECKSUM_VALIDATION); ClientAppId = GetClientAppIdEnvironmentVariable(); + AuthSchemePreference = GetCommaDelimitedEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_AUTH_SCHEME_PREFERENCE); + SigV4aSigningRegionSet = GetCommaDelimitedEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_SIGV4A_SIGNING_REGION_SET); } private bool GetEnvironmentVariable(string name, bool defaultValue) @@ -266,6 +281,20 @@ private string GetEC2MetadataEndpointEnvironmentVariable() return rawValue; } + /// + /// Loads a string value from an environment variable. + /// + /// The environment variable value or null if not set + private string GetStringEnvironmentVariable(string environmentVariableName) + { + if (!TryGetEnvironmentVariable(environmentVariableName, out var rawValue)) + { + return null; + } + + return rawValue; + } + /// /// Loads client app id from the environment variable. /// Throws an exception if the length of client app id is longer than 50. @@ -285,6 +314,25 @@ private string GetClientAppIdEnvironmentVariable() return rawValue; } + + /// + /// Parses an environment variable containing a comma delimited list into a list of strings. + /// Whitespace between names is ignored + /// + private List GetCommaDelimitedEnvironmentVariable(string environmentVariableName) + { + if (!TryGetEnvironmentVariable(environmentVariableName, out var rawValue)) + { + return null; + } + + var values = rawValue.Split(',') + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToList(); + + return values.Count > 0 ? values : null; + } } /// @@ -340,6 +388,8 @@ private void Setup(ICredentialProfileSource source, string profileName) AccountIdEndpointMode = profile.AccountIdEndpointMode; RequestChecksumCalculation = profile.RequestChecksumCalculation; ResponseChecksumValidation = profile.ResponseChecksumValidation; + AuthSchemePreference = profile.AuthSchemePreference; + SigV4aSigningRegionSet = profile.SigV4aSigningRegionSet; } else { @@ -365,6 +415,8 @@ private void Setup(ICredentialProfileSource source, string profileName) new KeyValuePair("account_id_endpoint_mode", profile.AccountIdEndpointMode), new KeyValuePair("request_checksum_calculation", profile.RequestChecksumCalculation), new KeyValuePair("response_checksum_validation", profile.ResponseChecksumValidation), + new KeyValuePair("auth_scheme_preference", profile.AuthSchemePreference), + new KeyValuePair("sigv4a_signing_region_set", profile.SigV4aSigningRegionSet), }; foreach(var item in items) @@ -436,6 +488,8 @@ public static void Reset() _cachedConfiguration.AccountIdEndpointMode = SeekValue(standardGenerators,(c) => c.AccountIdEndpointMode); _cachedConfiguration.RequestChecksumCalculation = SeekValue(standardGenerators, (c) => c.RequestChecksumCalculation); _cachedConfiguration.ResponseChecksumValidation = SeekValue(standardGenerators, (c) => c.ResponseChecksumValidation); + _cachedConfiguration.AuthSchemePreference = SeekList(standardGenerators, (c) => c.AuthSchemePreference); + _cachedConfiguration.SigV4aSigningRegionSet = SeekList(standardGenerators, (c) => c.SigV4aSigningRegionSet); } private static T? SeekValue(List generators, Func getValue) where T : struct @@ -470,8 +524,24 @@ private static string SeekString(List generators, Func SeekList(List generators, Func> getValue) + { + // Look for the configuration value stopping at the first generator that returns the expected value. + foreach (var generator in generators) + { + var configuration = generator(); + List value = getValue(configuration); + if (value != null && value.Count > 0) + { + return value; + } + } + + return null; + } + /// - /// Flag that specifies if endpoint discovery is enabled, disabled, + /// Flag that specifies if endpoint discovery is enabled, disabled, /// or not set. /// public static bool? EndpointDiscoveryEnabled @@ -634,5 +704,17 @@ public static ResponseChecksumValidation? ResponseChecksumValidation return _cachedConfiguration.ResponseChecksumValidation; } } + + /// + /// Preference list of authentication schemes to use when multiple schemes are available. + /// Parsed from comma-separated format at load time. + /// + public static List AuthSchemePreference => _cachedConfiguration.AuthSchemePreference; + + /// + /// The region set to use for SigV4a signing. + /// Parsed from comma-separated format at load time. + /// + public static List SigV4aSigningRegionSet => _cachedConfiguration.SigV4aSigningRegionSet; } } diff --git a/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseAuthResolverHandler.cs b/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseAuthResolverHandler.cs index 7e8c024b3a84..b55484ac61d7 100644 --- a/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseAuthResolverHandler.cs +++ b/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseAuthResolverHandler.cs @@ -56,6 +56,9 @@ protected void PreInvoke(IExecutionContext executionContext) throw new AmazonClientException($"No valid authentication schemes defined for {executionContext.RequestContext.RequestName}"); } + // Apply auth scheme preference if configured + authOptions = ApplyAuthSchemePreference(authOptions, executionContext.RequestContext.ClientConfig); + var clientConfig = executionContext.RequestContext.ClientConfig; var defaultCredentials = executionContext.RequestContext.ExplicitAWSCredentials ?? clientConfig.DefaultAWSCredentials; @@ -65,7 +68,7 @@ protected void PreInvoke(IExecutionContext executionContext) if (scheme == null) { // Current auth scheme option is not enabled / supported, continue iterating. - Logger.DebugFormat($"{authOptions[i].SchemeId} scheme is not supported for {executionContext.RequestContext.RequestName}"); + Logger?.DebugFormat($"{authOptions[i].SchemeId} scheme is not supported for {executionContext.RequestContext.RequestName}"); continue; } @@ -118,7 +121,7 @@ protected void PreInvoke(IExecutionContext executionContext) var areSchemesLeft = i < authOptions.Count - 1; if (areSchemesLeft) { - Logger.DebugFormat($"Could not resolve identity for {executionContext.RequestContext.RequestName} using {scheme.SchemeId} scheme: {ex.Message}"); + Logger?.DebugFormat($"Could not resolve identity for {executionContext.RequestContext.RequestName} using {scheme.SchemeId} scheme: {ex.Message}"); continue; } @@ -142,6 +145,9 @@ protected async Task PreInvokeAsync(IExecutionContext executionContext) throw new AmazonClientException($"No valid authentication schemes defined for {executionContext.RequestContext.RequestName}"); } + // Apply auth scheme preference if configured + authOptions = ApplyAuthSchemePreference(authOptions, executionContext.RequestContext.ClientConfig); + var clientConfig = executionContext.RequestContext.ClientConfig; var cancellationToken = executionContext.RequestContext.CancellationToken; var defaultCredentials = executionContext.RequestContext.ExplicitAWSCredentials ?? clientConfig.DefaultAWSCredentials; @@ -152,7 +158,7 @@ protected async Task PreInvokeAsync(IExecutionContext executionContext) if (scheme == null) { // Current auth scheme option is not enabled / supported, continue iterating. - Logger.DebugFormat($"{authOptions[i].SchemeId} scheme is not supported for {executionContext.RequestContext.RequestName}"); + Logger?.DebugFormat($"{authOptions[i].SchemeId} scheme is not supported for {executionContext.RequestContext.RequestName}"); continue; } @@ -200,7 +206,7 @@ protected async Task PreInvokeAsync(IExecutionContext executionContext) var areSchemesLeft = i < authOptions.Count - 1; if (areSchemesLeft) { - Logger.DebugFormat($"Could not resolve identity for {executionContext.RequestContext.RequestName} using {scheme.SchemeId} scheme: {ex.Message}"); + Logger?.DebugFormat($"Could not resolve identity for {executionContext.RequestContext.RequestName} using {scheme.SchemeId} scheme: {ex.Message}"); continue; } @@ -283,6 +289,85 @@ protected static List RetrieveSchemesFromEndpoint(Endpoint en /// protected abstract List ResolveAuthOptions(IExecutionContext executionContext); + /// + /// Applies the configured auth scheme preference to reorder the auth options. + /// + private static List ApplyAuthSchemePreference(List authOptions, IClientConfig clientConfig) + { + var preferences = ((ClientConfig)clientConfig).AuthSchemePreferenceList; + + if (preferences == null || preferences.Count == 0) + { + return authOptions; + } + + // Preferences are already trimmed and deduped during property set + + // Reorder auth options based on preferences + var reorderedOptions = new List(); + + // First, add options that match the preference order + foreach (var preferredScheme in preferences) + { + // Convert short name to full scheme ID (e.g., "sigv4" -> "aws.auth#sigv4") + var fullSchemeId = GetFullSchemeId(preferredScheme); + + foreach (var option in authOptions) + { + if (option.SchemeId == fullSchemeId && !reorderedOptions.Contains(option)) + { + reorderedOptions.Add(option); + } + } + } + + // Then add any remaining options that weren't in the preference list + foreach (var option in authOptions) + { + if (!reorderedOptions.Contains(option)) + { + reorderedOptions.Add(option); + } + } + + // CRITICAL: Ensure NoAuth/Anonymous is always last in the list. + // This prevents unauthenticated requests when authentication is available. + var noAuthOption = reorderedOptions.FirstOrDefault(o => + o.SchemeId == "smithy.api#noAuth" || + o.SchemeId.EndsWith("#noAuth")); + + if (noAuthOption != null) + { + reorderedOptions.Remove(noAuthOption); + reorderedOptions.Add(noAuthOption); + } + + return reorderedOptions; + } + + /// + /// Converts a short auth scheme name to its full scheme ID. + /// + private static string GetFullSchemeId(string schemeName) + { + // Handle common auth scheme names (case-sensitive as per specification) + if (schemeName == "sigv4") + return "aws.auth#sigv4"; + if (schemeName == "sigv4a") + return "aws.auth#sigv4a"; + if (schemeName == "httpBearerAuth" || schemeName == "bearer") + return "smithy.api#httpBearerAuth"; + if (schemeName == "noAuth") + return "smithy.api#noAuth"; + + // If it already looks like a full ID (contains #), return as-is + if (schemeName.Contains("#")) + return schemeName; + + // Otherwise, assume it's an AWS auth scheme + return $"aws.auth#{schemeName}"; + } + private static void AddUserAgentDetails(IExecutionContext executionContext) { var requestContext = executionContext.RequestContext; diff --git a/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs b/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs index 5991a79634aa..32fb76374e86 100644 --- a/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs +++ b/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs @@ -78,12 +78,12 @@ public virtual void ProcessRequestHandlers(IExecutionContext executionContext) } // set authentication parameters and headers - SetAuthenticationAndHeaders(requestContext.Request, endpoint); + SetAuthenticationAndHeaders(executionContext, endpoint); // service-specific handling, code-generated ServiceSpecificHandler(executionContext, parameters); - // override AuthenticationRegion from ClientConfig if specified + // AuthenticationRegion has highest priority, overrides everything if (!string.IsNullOrEmpty(config.AuthenticationRegion)) { requestContext.Request.AuthenticationRegion = config.AuthenticationRegion; @@ -129,8 +129,10 @@ protected virtual void ServiceSpecificHandler(IExecutionContext executionContext } private static readonly string[] SupportedAuthSchemas = { "sigv4-s3express", "sigv4", "sigv4a" }; - private static void SetAuthenticationAndHeaders(IRequest request, Endpoint endpoint) + private static void SetAuthenticationAndHeaders(IExecutionContext executionContext, Endpoint endpoint) { + var request = executionContext.RequestContext.Request; + var config = executionContext.RequestContext.ClientConfig; if (endpoint.Attributes != null) { var authSchemes = (IList)endpoint.Attributes["authSchemes"]; @@ -203,6 +205,21 @@ private static void SetAuthenticationAndHeaders(IRequest request, Endpoint endpo request.Headers[header.Key] = string.Join(",", header.Value.ToArray()); } } + + if (executionContext.RequestContext.Signer is AWS4aSignerCRTWrapper) + { + request.SignatureVersion = SignatureVersion.SigV4a; + + var userRegionSet = ((ClientConfig)config).SigV4aSigningRegionSetList; + if (userRegionSet != null && userRegionSet.Count > 0) + { + var authenticationRegion = string.Join(",", userRegionSet); + if (!string.IsNullOrEmpty(authenticationRegion)) + { + request.AuthenticationRegion = authenticationRegion; + } + } + } } private static void ApplyCommonSchema(IRequest request, PropertyBag schema) diff --git a/sdk/test/NetStandard/UnitTests/ClientConfigTests.cs b/sdk/test/NetStandard/UnitTests/ClientConfigTests.cs index 21b251fd089c..b8a361601d44 100644 --- a/sdk/test/NetStandard/UnitTests/ClientConfigTests.cs +++ b/sdk/test/NetStandard/UnitTests/ClientConfigTests.cs @@ -72,7 +72,9 @@ public class ClientConfigTests "RequestChecksumCalculation", "ResponseChecksumValidation", "IdentityResolverConfiguration", - "DefaultAWSCredentials" + "DefaultAWSCredentials", + "AuthSchemePreference", + "SigV4aSigningRegionSet" }; [Fact] diff --git a/sdk/test/UnitTests/Custom/Runtime/AuthCredentialResolutionTests.cs b/sdk/test/UnitTests/Custom/Runtime/AuthCredentialResolutionTests.cs new file mode 100644 index 000000000000..dd7343ba1fb8 --- /dev/null +++ b/sdk/test/UnitTests/Custom/Runtime/AuthCredentialResolutionTests.cs @@ -0,0 +1,435 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.Runtime.Credentials.Internal; +using Amazon.Runtime.Endpoints; +using Amazon.Runtime.Internal; +using Amazon.Runtime.Internal.Auth; +using Amazon.Runtime.Identity; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AWSSDK.UnitTests.Runtime +{ + /// + /// Tests for auth scheme resolution with credential availability. + /// These tests verify that an auth scheme is only considered "supported" when BOTH: + /// 1. The SDK has an implementation for the auth scheme (signer) + /// 2. The SDK has an identity provider registered for the auth scheme (credentials) + /// + /// The separation between implementation and identity: + /// - Implementation: The signer that knows HOW to sign requests (e.g., AWS4Signer, BearerTokenSigner) + /// - Identity: The credentials/token that provides WHAT to sign with (e.g., AWS credentials, bearer token) + /// + /// Common scenarios: + /// - SigV4 without AWS credentials: Cannot use SigV4 + /// - Bearer auth without a token provider: Cannot use bearer auth + /// - Multiple auth schemes available: Use first one with BOTH implementation and identity + /// + [TestClass] + public class AuthCredentialResolutionTests : RuntimePipelineTestBase + { + #region Resolving Auth and Credentials Tests + + /// + /// Runtime supports both, identity providers for both, service lists sigv4 first. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void AuthCredentials_BothRuntimeAndIdentity_UsesFirstInList() + { + var serviceAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4" }, + new AuthSchemeOption { SchemeId = "smithy.api#httpBearerAuth" } + }; + + // Both credentials and token provider available + var context = CreateMockContextWithBothIdentities(); + var resolver = new TestAuthResolver(serviceAuthOptions); + + resolver.PreInvoke(context); + + Assert.IsNotNull(context.RequestContext.Identity, "Identity should be resolved"); + Assert.IsNotNull(context.RequestContext.Signer, "Signer should be set"); + // Should use sigv4 (first in service list) + Assert.AreEqual("AWS4Signer", context.RequestContext.Signer.GetType().Name); + } + + /// + /// Only bearer identity available, so use bearer. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void AuthCredentials_OnlyBearerIdentityAvailable_UsesBearer() + { + var serviceAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4" }, + new AuthSchemeOption { SchemeId = "smithy.api#httpBearerAuth" } + }; + + // Only bearer token available, no AWS credentials + var context = CreateMockContextBearerOnly(); + var resolver = new TestAuthResolver(serviceAuthOptions); + + resolver.PreInvoke(context); + + Assert.IsNotNull(context.RequestContext.Identity, "Identity should be resolved"); + Assert.IsNotNull(context.RequestContext.Signer, "Signer should be set"); + // Should skip sigv4 (no credentials) and use bearer + Assert.AreEqual("BearerTokenSigner", context.RequestContext.Signer.GetType().Name); + } + + /// + /// No identity providers available for any supported auth scheme. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + [ExpectedException(typeof(AmazonClientException))] + public void AuthCredentials_NoIdentityProviders_ThrowsError() + { + var serviceAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4" }, + new AuthSchemeOption { SchemeId = "smithy.api#httpBearerAuth" } + }; + + // No credentials or token provider + var context = CreateMockContextNoIdentity(); + var resolver = new TestAuthResolver(serviceAuthOptions); + + // Should throw because no identity providers are available + resolver.PreInvoke(context); + } + + + #endregion + + #region Helper Methods and Test Infrastructure + + private IExecutionContext CreateMockContextWithBothIdentities() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // Provide both AWS credentials AND bearer token + config.DefaultAWSCredentials = new BasicAWSCredentials("accessKey", "secretKey"); + config.AWSTokenProvider = new MockTokenProvider(); + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + private IExecutionContext CreateMockContextCredentialsOnly() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // Only provide AWS credentials, no bearer token + config.DefaultAWSCredentials = new BasicAWSCredentials("accessKey", "secretKey"); + config.AWSTokenProvider = null; + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + private IExecutionContext CreateMockContextBearerOnly() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // Only provide bearer token, no AWS credentials + config.DefaultAWSCredentials = null; + config.AWSTokenProvider = new MockTokenProvider(); + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + private IExecutionContext CreateMockContextNoIdentity() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // No credentials or token provider + config.DefaultAWSCredentials = null; + config.AWSTokenProvider = null; + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + /// + /// Test implementation of BaseAuthResolverHandler that provides test auth options. + /// The base class already handles checking for both runtime AND identity. + /// + private class TestAuthResolver : BaseAuthResolverHandler + { + private readonly List _authOptions; + + public TestAuthResolver(List authOptions) + { + _authOptions = authOptions ?? new List(); + } + + protected override List ResolveAuthOptions(IExecutionContext executionContext) + { + return _authOptions; + } + + // Public wrapper for testing - exposes the protected PreInvoke method + public new void PreInvoke(IExecutionContext executionContext) + { + base.PreInvoke(executionContext); + } + } + + /// + /// Mock token provider for bearer auth tests + /// + private class MockTokenProvider : IAWSTokenProvider + { +#if BCL + public bool TryResolveToken(out AWSToken token) + { + token = new AWSToken { Token = "mock-token", Expiration = DateTime.UtcNow.AddHours(1) }; + return true; + } +#endif + + public Task> TryResolveTokenAsync(CancellationToken cancellationToken = default) + { + var token = new AWSToken { Token = "mock-token", Expiration = DateTime.UtcNow.AddHours(1) }; + return Task.FromResult(new TryResponse { Success = true, Value = token }); + } + } + + /// + /// Mock request class for testing. + /// + private class MockAmazonWebServiceRequest : AmazonWebServiceRequest + { + } + + /// + /// Mock client configuration for testing. + /// + private class MockClientConfig : ClientConfig + { + private bool _allowCredentialResolution = true; + + public MockClientConfig() : base(new DummyDefaultConfigurationProvider()) + { + RegionEndpoint = Amazon.RegionEndpoint.USEast1; + // Use a custom identity resolver configuration that respects our test settings + this.IdentityResolverConfiguration = new MockIdentityResolverConfiguration(this); + } + + public void DisableCredentialResolution() + { + _allowCredentialResolution = false; + } + + public bool ShouldResolveCredentials => _allowCredentialResolution; + + public override string RegionEndpointServiceName => "test"; + public override string ServiceVersion => "1.0"; + public override string UserAgent => "test-agent"; + + public override Endpoint DetermineServiceOperationEndpoint(ServiceOperationEndpointParameters parameters) + { + // For testing, return a simple endpoint + return new Endpoint("https://test.amazonaws.com"); + } + + private class DummyDefaultConfigurationProvider : IDefaultConfigurationProvider + { + public IDefaultConfiguration GetDefaultConfiguration( + RegionEndpoint clientRegion, + DefaultConfigurationMode? requestedConfigurationMode = null) + { + return new DefaultConfiguration(); + } + } + } + + /// + /// Mock identity resolver configuration that respects test settings. + /// + private class MockIdentityResolverConfiguration : IIdentityResolverConfiguration + { + private readonly MockClientConfig _config; + + public MockIdentityResolverConfiguration(MockClientConfig config) + { + _config = config; + } + + public IIdentityResolver GetIdentityResolver() where T : BaseIdentity + { + if (typeof(T) == typeof(AWSCredentials)) + { + return new MockAWSCredentialsResolver(_config); + } + if (typeof(T) == typeof(AWSToken)) + { + return new MockAWSTokenResolver(_config); + } + if (typeof(T) == typeof(AnonymousAWSCredentials)) + { + return new AnonymousIdentityResolver(); + } + throw new NotImplementedException($"{typeof(T).Name} is not supported"); + } + } + + /// + /// Mock AWS credentials resolver that only returns credentials when explicitly set. + /// + private class MockAWSCredentialsResolver : IIdentityResolver + { + private readonly MockClientConfig _config; + + public MockAWSCredentialsResolver(MockClientConfig config) + { + _config = config; + } + + public AWSCredentials ResolveIdentity(IClientConfig clientConfig) + { + // Only return credentials if they were explicitly set + if (_config.ShouldResolveCredentials && _config.DefaultAWSCredentials != null) + { + return _config.DefaultAWSCredentials; + } + return null; + } + + public Task ResolveIdentityAsync(IClientConfig clientConfig, CancellationToken cancellationToken = default) + { + return Task.FromResult(ResolveIdentity(clientConfig)); + } + + BaseIdentity IIdentityResolver.ResolveIdentity(IClientConfig clientConfig) => ResolveIdentity(clientConfig); + + Task IIdentityResolver.ResolveIdentityAsync(IClientConfig clientConfig, CancellationToken cancellationToken) + => Task.FromResult(ResolveIdentity(clientConfig)); + } + + /// + /// Mock AWS token resolver that only returns token when explicitly set. + /// + private class MockAWSTokenResolver : IIdentityResolver + { + private readonly MockClientConfig _config; + + public MockAWSTokenResolver(MockClientConfig config) + { + _config = config; + } + + public AWSToken ResolveIdentity(IClientConfig clientConfig) + { + // Only return token if token provider was explicitly set + if (_config.AWSTokenProvider != null) + { +#if BCL + if (_config.AWSTokenProvider.TryResolveToken(out var token)) + { + return token; + } +#else + var result = _config.AWSTokenProvider.TryResolveTokenAsync().GetAwaiter().GetResult(); + if (result.Success) + { + return result.Value; + } +#endif + } + return null; + } + + public async Task ResolveIdentityAsync(IClientConfig clientConfig, CancellationToken cancellationToken = default) + { + // Only return token if token provider was explicitly set + if (_config.AWSTokenProvider != null) + { + var result = await _config.AWSTokenProvider.TryResolveTokenAsync(cancellationToken).ConfigureAwait(false); + if (result.Success) + { + return result.Value; + } + } + return null; + } + + BaseIdentity IIdentityResolver.ResolveIdentity(IClientConfig clientConfig) => ResolveIdentity(clientConfig); + + async Task IIdentityResolver.ResolveIdentityAsync(IClientConfig clientConfig, CancellationToken cancellationToken) + => await ResolveIdentityAsync(clientConfig, cancellationToken).ConfigureAwait(false); + } + + #endregion + } +} \ No newline at end of file diff --git a/sdk/test/UnitTests/Custom/Runtime/AuthSchemePreferenceTests.cs b/sdk/test/UnitTests/Custom/Runtime/AuthSchemePreferenceTests.cs new file mode 100644 index 000000000000..2380ebd6b316 --- /dev/null +++ b/sdk/test/UnitTests/Custom/Runtime/AuthSchemePreferenceTests.cs @@ -0,0 +1,175 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon; +using Amazon.Runtime; +using Amazon.Runtime.Credentials.Internal; +using Amazon.Runtime.Endpoints; +using Amazon.Runtime.Internal; +using Amazon.Runtime.Internal.Auth; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AWSSDK.UnitTests +{ + /// + /// Test cases for authentication scheme preference configuration. + /// These tests verify that users can configure their preferred authentication schemes + /// and that the SDK correctly reorders authentication options based on those preferences. + /// + /// Key behaviors tested: + /// - The preference list reorders available auth schemes, not overrides them + /// - Unsupported auth schemes in the preference list are ignored + /// - The SDK maintains security by always placing noAuth last + /// + [TestClass] + public class AuthSchemePreferenceTests + { + // Cache reflection info to avoid repeated lookups + private static readonly Type _baseAuthResolverHandlerType = typeof(BaseAuthResolverHandler); + private static readonly MethodInfo _applyAuthSchemePreferenceMethod; + + static AuthSchemePreferenceTests() + { + // Note for future maintainers: ApplyAuthSchemePreference is a private method + // We use reflection here for unit testing without exposing internals + _applyAuthSchemePreferenceMethod = _baseAuthResolverHandlerType.GetMethod( + "ApplyAuthSchemePreference", + BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + + Assert.IsNotNull(_applyAuthSchemePreferenceMethod, + "ApplyAuthSchemePreference method not found - has the method been renamed or moved?"); + } + + /// + /// Test that multiple schemes in preference list are applied in order. + /// Expected: Auth schemes are reordered to match preference list order. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestPreferenceList_MultipleSchemes_RespectsOrder() + { + var authOptions = new List + { + new AuthSchemeOption { SchemeId = AuthSchemeOption.SigV4 }, + new AuthSchemeOption { SchemeId = AuthSchemeOption.SigV4A } + }; + + var config = new TestClientConfig(); + config.AuthSchemePreference = "sigv4a, sigv4"; + + var result = ApplyAuthSchemePreference(authOptions, config); + + Assert.AreEqual(2, result.Count); + Assert.AreEqual(AuthSchemeOption.SigV4A, result[0].SchemeId); + Assert.AreEqual(AuthSchemeOption.SigV4, result[1].SchemeId); + } + + + /// + /// Test that noAuth is always placed last even when it's the only preference. + /// This verifies the critical security requirement in all scenarios. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestPreferenceList_NoAuthOnly_AlwaysLast() + { + var authOptions = new List + { + new AuthSchemeOption { SchemeId = "smithy.api#httpBearerAuth" }, + new AuthSchemeOption { SchemeId = "smithy.api#noAuth" }, + new AuthSchemeOption { SchemeId = AuthSchemeOption.SigV4 } + }; + + var config = new TestClientConfig(); + config.AuthSchemePreference = "noAuth"; // Only noAuth in preference + + var result = ApplyAuthSchemePreference(authOptions, config); + + Assert.AreEqual(3, result.Count); + // SECURITY: noAuth must be last, other schemes maintain their original order + Assert.AreEqual("smithy.api#httpBearerAuth", result[0].SchemeId); + Assert.AreEqual(AuthSchemeOption.SigV4, result[1].SchemeId); + Assert.AreEqual("smithy.api#noAuth", result[2].SchemeId); // Always last! + } + + #region Helper Methods + + /// + /// Helper method to invoke the private ApplyAuthSchemePreference method. + /// + private List ApplyAuthSchemePreference( + List authOptions, + IClientConfig config) + { + // Create a mock handler instance for invoking the method + var handler = new TestAuthResolverHandler(); + var result = _applyAuthSchemePreferenceMethod.Invoke( + handler, + new object[] { authOptions, config }); + + return (List)result; + } + + /// + /// Test implementation of BaseAuthResolverHandler for unit testing. + /// + private class TestAuthResolverHandler : BaseAuthResolverHandler + { + protected override List ResolveAuthOptions(IExecutionContext executionContext) + { + // Not used in these tests + throw new NotImplementedException(); + } + } + + /// + /// Test client configuration for unit testing. + /// + private class TestClientConfig : ClientConfig + { + public TestClientConfig() : base(new DummyDefaultConfigurationProvider()) + { + this.RegionEndpoint = RegionEndpoint.USEast1; + } + + public override string RegionEndpointServiceName { get; } = "TestService"; + public override string ServiceVersion { get; } = "1.0"; + public override string UserAgent => "TestUserAgent"; + + public override Endpoint DetermineServiceOperationEndpoint(ServiceOperationEndpointParameters parameters) + { + return new Endpoint(this.ServiceURL ?? "https://example.com"); + } + + private class DummyDefaultConfigurationProvider : IDefaultConfigurationProvider + { + public IDefaultConfiguration GetDefaultConfiguration( + RegionEndpoint clientRegion, + DefaultConfigurationMode? requestedConfigurationMode = null) + { + return new DefaultConfiguration(); + } + } + } + + #endregion + } +} \ No newline at end of file diff --git a/sdk/test/UnitTests/Custom/Runtime/C2JAuthResolutionTests.cs b/sdk/test/UnitTests/Custom/Runtime/C2JAuthResolutionTests.cs new file mode 100644 index 000000000000..a43c52035892 --- /dev/null +++ b/sdk/test/UnitTests/Custom/Runtime/C2JAuthResolutionTests.cs @@ -0,0 +1,277 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Runtime; +using Amazon.Runtime.Credentials.Internal; +using Amazon.Runtime.Identity; +using Amazon.Runtime.Internal; +using Amazon.Runtime.Internal.Auth; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AWSSDK.UnitTests.Runtime +{ + /// + /// Tests for C2J Auth Type Resolution. + /// These tests verify the auth scheme resolution logic when working with C2J service models, + /// which are used by many AWS services in the .NET SDK. + /// + /// Key behaviors tested: + /// - Service-level auth configuration + /// - Operation-level auth overrides + /// - "First supported wins" resolution model + /// - Handling of multiple auth types (sigv4, sigv4a, bearer, noauth) + /// + [TestClass] + public class C2JAuthResolutionTests : RuntimePipelineTestBase + { + #region SigV4/SigV4a Resolution Tests + + /// + /// Test: When client supports both and service lists sigv4a first, sigv4a should be chosen. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void C2J_ClientSupportsV4andV4a_ServiceSupportsV4aThenV4_NoOperationOverride_ResolvesToV4a() + { + var serviceAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4a" }, + new AuthSchemeOption { SchemeId = "aws.auth#sigv4" } + }; + + var context = CreateMockContext(); + var resolver = new TestAuthResolver(serviceAuthOptions); + + resolver.PreInvoke(context); + + Assert.IsNotNull(context.RequestContext.Identity, "Identity should be resolved"); + Assert.IsNotNull(context.RequestContext.Signer, "Signer should be set"); + // SigV4a should be selected (AWS4aSignerCRTWrapper) + Assert.AreEqual("AWS4aSignerCRTWrapper", context.RequestContext.Signer.GetType().Name); + } + + /// + /// Test: When operation overrides auth order, the operation's order takes precedence. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void C2J_ClientSupportsV4andV4a_OperationOverridesWithV4aThenV4_ResolvesToV4a() + { + // Operation override - sigv4a comes first + var operationAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4a" }, + new AuthSchemeOption { SchemeId = "aws.auth#sigv4" } + }; + + var context = CreateMockContext(); + var resolver = new TestAuthResolver(operationAuthOptions); + + resolver.PreInvoke(context); + + Assert.IsNotNull(context.RequestContext.Identity, "Identity should be resolved"); + Assert.IsNotNull(context.RequestContext.Signer, "Signer should be set"); + // SigV4a should be selected (AWS4aSignerCRTWrapper) + Assert.AreEqual("AWS4aSignerCRTWrapper", context.RequestContext.Signer.GetType().Name); + } + + /// + /// Test: When client only supports sigv4 and service lists sigv4a first, still use sigv4. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void C2J_ClientSupportsOnlyV4_ServiceListsV4aFirst_StillResolvesToV4() + { + var serviceAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4a" }, + new AuthSchemeOption { SchemeId = "aws.auth#sigv4" } + }; + + var context = CreateMockContext(); + var unsupportedSchemes = new HashSet { "aws.auth#sigv4a" }; + var resolver = new TestAuthResolver(serviceAuthOptions, unsupportedSchemes); + + resolver.PreInvoke(context); + + Assert.IsNotNull(context.RequestContext.Identity, "Identity should be resolved"); + Assert.IsNotNull(context.RequestContext.Signer, "Signer should be set"); + // SigV4 should be selected (AWS4Signer) - client doesn't support V4a + Assert.AreEqual("AWS4Signer", context.RequestContext.Signer.GetType().Name); + } + + /// + /// Test: When client doesn't support the only auth type specified by operation, it should error. + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + [ExpectedException(typeof(AmazonClientException))] + public void C2J_ClientSupportsOnlyV4_OperationRequiresOnlyV4a_ThrowsError() + { + var operationAuthOptions = new List + { + new AuthSchemeOption { SchemeId = "aws.auth#sigv4a" } + }; + + var context = CreateMockContext(); + var unsupportedSchemes = new HashSet { "aws.auth#sigv4a" }; + var resolver = new TestAuthResolver(operationAuthOptions, unsupportedSchemes); + + // This should throw an exception - V4a not supported + resolver.PreInvoke(context); + } + + #endregion + + + #region Helper Methods and Test Infrastructure + + private IExecutionContext CreateMockContext() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // Provide mock credentials for identity resolution + config.DefaultAWSCredentials = new BasicAWSCredentials("accessKey", "secretKey"); + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + private IExecutionContext CreateMockContextBearer() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // Provide token provider for bearer auth (not credentials) + config.AWSTokenProvider = new MockTokenProvider(); + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + private IExecutionContext CreateMockContextNoAuth() + { + var originalRequest = new MockAmazonWebServiceRequest(); + var request = new Amazon.Runtime.Internal.DefaultRequest(originalRequest, "TestService"); + request.Endpoint = new Uri("https://test.amazonaws.com"); + + var config = new MockClientConfig(); + // No credentials or token provider for NoAuth + + var requestContext = new RequestContext(true, new NullSigner()) + { + OriginalRequest = originalRequest, + Request = request, + ClientConfig = config + }; + + var responseContext = new ResponseContext(); + + return new Amazon.Runtime.Internal.ExecutionContext(requestContext, responseContext); + } + + /// + /// Test implementation of BaseAuthResolverHandler that provides test auth options. + /// Allows controlling which auth schemes are "supported" to simulate scenarios like + /// CRT not being available for V4a. + /// + private class TestAuthResolver : BaseAuthResolverHandler + { + private readonly List _authOptions; + private readonly HashSet _unsupportedSchemes; + + public TestAuthResolver(List authOptions, HashSet unsupportedSchemes = null) + { + // Store the auth options to return from ResolveAuthOptions + _authOptions = authOptions ?? new List(); + _unsupportedSchemes = unsupportedSchemes ?? new HashSet(); + } + + protected override List ResolveAuthOptions(IExecutionContext executionContext) + { + return _authOptions; + } + + protected override ISigner GetSigner(IAuthScheme scheme) + { + // Simulate scheme not being supported (e.g., CRT not available for V4a) + if (_unsupportedSchemes.Contains(scheme.SchemeId)) + { + throw new AmazonClientException($"Simulated: {scheme.SchemeId} is not supported in this test configuration"); + } + return base.GetSigner(scheme); + } + + // Public wrapper for testing - exposes the protected PreInvoke method + public new void PreInvoke(IExecutionContext executionContext) + { + base.PreInvoke(executionContext); + } + } + + /// + /// Mock token provider for bearer auth tests + /// + private class MockTokenProvider : IAWSTokenProvider + { +#if BCL + public bool TryResolveToken(out AWSToken token) + { + token = new AWSToken { Token = "mock-token", Expiration = DateTime.UtcNow.AddHours(1) }; + return true; + } +#endif + + public Task> TryResolveTokenAsync(CancellationToken cancellationToken = default) + { + var token = new AWSToken { Token = "mock-token", Expiration = DateTime.UtcNow.AddHours(1) }; + return Task.FromResult(new TryResponse { Success = true, Value = token }); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/sdk/test/UnitTests/Custom/Runtime/SigV4aSigningRegionSetTests.cs b/sdk/test/UnitTests/Custom/Runtime/SigV4aSigningRegionSetTests.cs new file mode 100644 index 000000000000..255ca70f5109 --- /dev/null +++ b/sdk/test/UnitTests/Custom/Runtime/SigV4aSigningRegionSetTests.cs @@ -0,0 +1,311 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon.Runtime; +using Amazon.Runtime.Internal; +using Amazon.Runtime.Endpoints; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Amazon; + +#if BCL || NETSTANDARD +using Amazon.Runtime.CredentialManagement; +#endif + +namespace AWSSDK.UnitTests +{ + /// + /// Test cases for SigV4a signing region set resolution. + /// These tests verify the configuration hierarchy for SigV4a signing region set resolution. + /// + /// Configuration hierarchy (highest to lowest precedence): + /// 1. Code (explicit configuration in client/request) + /// 2. Environment variable (AWS_SIGV4A_SIGNING_REGION_SET) + /// 3. Config file (sigv4a_signing_region_set in profile) + /// 4. Endpoints 2.0 metadata (from endpoint resolution) + /// 5. Endpoint region (default to the configured region) + /// + /// Notes: + /// - "*" indicates the request is valid in all regions + /// - Multiple regions are comma-separated + /// - This feature enables multi-region request signing for global services + /// + [TestClass] + public class SigV4aSigningRegionSetTests + { + private const string TEST_PROFILE = "test-sigv4a-profile"; + private string _originalEnvironmentVariable; + + [TestInitialize] + public void TestInitialize() + { + // Save original environment variable + _originalEnvironmentVariable = Environment.GetEnvironmentVariable( + EnvironmentVariableInternalConfiguration.ENVIRONMENT_VARIABLE_AWS_SIGV4A_SIGNING_REGION_SET); + } + + [TestCleanup] + public void TestCleanup() + { + // Restore original environment variable + if (_originalEnvironmentVariable != null) + { + Environment.SetEnvironmentVariable( + EnvironmentVariableInternalConfiguration.ENVIRONMENT_VARIABLE_AWS_SIGV4A_SIGNING_REGION_SET, + _originalEnvironmentVariable); + } + else + { + Environment.SetEnvironmentVariable( + EnvironmentVariableInternalConfiguration.ENVIRONMENT_VARIABLE_AWS_SIGV4A_SIGNING_REGION_SET, + null); + } + } + + /// + /// Endpoint Region | Endpoints 2.0 Metadata | Environment | Config File | Code | Result + /// us-west-2 | n/a | n/a | n/a | n/a | us-west-2 + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestSigV4aRegionSet_DefaultToEndpointRegion() + { + var config = CreateTestConfig("us-west-2"); + + var regionSet = ResolveSigV4aSigningRegionSet(config, null, null, null, null); + + Assert.AreEqual("us-west-2", regionSet); + } + + /// + /// Endpoint Region | Endpoints 2.0 Metadata | Environment | Config File | Code | Result + /// us-west-2 | * | n/a | n/a | n/a | * + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestSigV4aRegionSet_Endpoints2Metadata_AllRegions() + { + var config = CreateTestConfig("us-west-2"); + var endpoint = CreateEndpointWithSigV4aMetadata("*"); + + var regionSet = ResolveSigV4aSigningRegionSet(config, endpoint, null, null, null); + + Assert.AreEqual("*", regionSet); + } + + /// + /// Endpoint Region | Endpoints 2.0 Metadata | Environment | Config File | Code | Result + /// us-west-2 | n/a | * | n/a | n/a | * + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestSigV4aRegionSet_EnvironmentVariable_AllRegions() + { + var config = CreateTestConfig("us-west-2"); + Environment.SetEnvironmentVariable( + EnvironmentVariableInternalConfiguration.ENVIRONMENT_VARIABLE_AWS_SIGV4A_SIGNING_REGION_SET, + "*"); + + var regionSet = ResolveSigV4aSigningRegionSet(config, null, "*", null, null); + + Assert.AreEqual("*", regionSet); + } + + /// + /// Endpoint Region | Endpoints 2.0 Metadata | Environment | Config File | Code | Result + /// us-west-2 | n/a | n/a | * | n/a | * + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestSigV4aRegionSet_ConfigFile_AllRegions() + { + var config = CreateTestConfig("us-west-2"); + + var regionSet = ResolveSigV4aSigningRegionSet(config, null, null, "*", null); + + Assert.AreEqual("*", regionSet); + } + + /// + /// Endpoint Region | Endpoints 2.0 Metadata | Environment | Config File | Code | Result + /// us-west-2 | n/a | n/a | n/a | * | * + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestSigV4aRegionSet_CodeConfig_AllRegions() + { + var config = CreateTestConfig("us-west-2"); + config.SigV4aSigningRegionSet = "*"; + + var regionSet = ResolveSigV4aSigningRegionSet(config, null, null, null, "*"); + + Assert.AreEqual("*", regionSet); + } + + /// + /// Endpoint Region | Endpoints 2.0 Metadata | Environment | Config File | Code | Result + /// us-west-2 | * | n/a | n/a | us-west-2 | us-west-2 + /// + [TestMethod] + [TestCategory("UnitTest")] + [TestCategory("Runtime")] + public void TestSigV4aRegionSet_CodeOverridesAll() + { + var config = CreateTestConfig("us-west-2"); + config.SigV4aSigningRegionSet = "us-west-2"; + var endpoint = CreateEndpointWithSigV4aMetadata("*"); + + var regionSet = ResolveSigV4aSigningRegionSet(config, endpoint, null, null, "us-west-2"); + + Assert.AreEqual("us-west-2", regionSet); + } + + #region Helper Methods + + /// + /// Create a test client configuration with specified region. + /// + private TestClientConfig CreateTestConfig(string region) + { + var config = new TestClientConfig(); + config.RegionEndpoint = RegionEndpoint.GetBySystemName(region); + return config; + } + + /// + /// Create an endpoint with SigV4a metadata for testing. + /// + private Endpoint CreateEndpointWithSigV4aMetadata(string signingRegionSet) + { + var attributes = new PropertyBag(); + + if (!string.IsNullOrEmpty(signingRegionSet)) + { + var authSchemes = new List(); + var sigv4aScheme = new PropertyBag(); + sigv4aScheme["name"] = "sigv4a"; + sigv4aScheme["signingName"] = "test-service"; + + // Parse region set - if it contains commas, split it into a list + if (signingRegionSet.Contains(",")) + { + var regions = signingRegionSet.Split(',').Select(r => r.Trim()).ToList(); + sigv4aScheme["signingRegionSet"] = regions; + } + else + { + sigv4aScheme["signingRegionSet"] = new List { signingRegionSet }; + } + + authSchemes.Add(sigv4aScheme); + attributes["authSchemes"] = authSchemes; + } + + return new Endpoint("https://test-service.amazonaws.com") + { + Attributes = attributes + }; + } + + /// + /// Simulates the resolution of SigV4a signing region set based on configuration hierarchy. + /// This mimics the actual resolution logic that would occur across various components of the SDK. + /// + private string ResolveSigV4aSigningRegionSet( + TestClientConfig config, + Endpoint endpoint, + string environmentValue, + string configFileValue, + string codeValue) + { + // Hierarchy: Code > Environment > Config File > Endpoints 2.0 > Default Region + + if (!string.IsNullOrEmpty(codeValue)) + { + return codeValue; + } + + if (!string.IsNullOrEmpty(environmentValue)) + { + return environmentValue; + } + + if (!string.IsNullOrEmpty(configFileValue)) + { + return configFileValue; + } + + if (endpoint?.Attributes != null && endpoint.Attributes["authSchemes"] != null) + { + var authSchemes = endpoint.Attributes["authSchemes"] as IList; + if (authSchemes != null && authSchemes.Count > 0) + { + var firstScheme = authSchemes[0] as PropertyBag; + if (firstScheme != null && firstScheme["signingRegionSet"] != null) + { + var regionSet = firstScheme["signingRegionSet"]; + if (regionSet is IList regionList) + { + return string.Join(",", regionList.Cast()); + } + return regionSet.ToString(); + } + } + } + + // Default to the endpoint region + return config.RegionEndpoint.SystemName; + } + + /// + /// Test client configuration for unit testing. + /// + private class TestClientConfig : ClientConfig + { + public TestClientConfig() : base(new DummyDefaultConfigurationProvider()) + { + } + + public override string RegionEndpointServiceName { get; } = "TestService"; + public override string ServiceVersion { get; } = "1.0"; + public override string UserAgent => "TestUserAgent"; + + public override Endpoint DetermineServiceOperationEndpoint(ServiceOperationEndpointParameters parameters) + { + return new Endpoint(this.ServiceURL ?? "https://example.com"); + } + + private class DummyDefaultConfigurationProvider : IDefaultConfigurationProvider + { + public IDefaultConfiguration GetDefaultConfiguration( + RegionEndpoint clientRegion, + DefaultConfigurationMode? requestedConfigurationMode = null) + { + return new DefaultConfiguration(); + } + } + } + + #endregion + } +} \ No newline at end of file