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