From 527f455f074bb9f621b790476ef8569997d99fa8 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 12:57:17 -0400 Subject: [PATCH 01/16] Prototype fix for respecting auth scheme preference. --- awscli/botocore/auth.py | 3 + awscli/botocore/handlers.py | 115 ++++++++++++++++++++++++++++++++++++ awscli/botocore/signers.py | 2 + 3 files changed, 120 insertions(+) diff --git a/awscli/botocore/auth.py b/awscli/botocore/auth.py index 8ceb4ff7dcbb..cfd001ad9cfb 100644 --- a/awscli/botocore/auth.py +++ b/awscli/botocore/auth.py @@ -915,11 +915,14 @@ def resolve_auth_scheme_preference(preference_list, auth_options): if scheme in service_supported ] + print(f'prioritized: {prioritized_schemes}') + for scheme in prioritized_schemes: if scheme == 'noAuth': return AUTH_PREF_TO_SIGNATURE_VERSION[scheme] sig_version = AUTH_PREF_TO_SIGNATURE_VERSION.get(scheme) if sig_version in AUTH_TYPE_MAPS: + print(f'sig version: {sig_version}') return sig_version raise UnsupportedSignatureVersionError( diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index 6bca2d9c4b6b..74445e1fffd8 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -50,6 +50,7 @@ AliasConflictParameterError, MissingServiceIdError, # noqa ParamValidationError, + UnsupportedSignatureVersionError, UnsupportedTLSVersionWarning, ) from botocore.regions import EndpointResolverBuiltins @@ -64,6 +65,7 @@ SAFE_CHARS, SERVICE_NAME_ALIASES, ArnParser, + ensure_boolean, get_token_from_environment, hyphenize_service_id, # noqa is_global_accesspoint, # noqa @@ -71,6 +73,8 @@ switch_host_with_param, ) +from awscli.botocore.auth import resolve_auth_scheme_preference + logger = logging.getLogger(__name__) REGISTER_FIRST = object() @@ -167,6 +171,13 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return auth_type if auth_type == 'v4a': + # Before committing to sigv4a, check if the user has configured + # an auth scheme override. This must be done here because + # emit_until_response stops after this function returns non-None, + # preventing _set_auth_scheme_preference_signer from running. + override = _resolve_auth_scheme_override(context, signing_name) + if override is not None: + return override # If sigv4a is chosen, we must add additional signing config for # global signature. region = _resolve_sigv4a_region(context) @@ -192,6 +203,110 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return signature_version +def _strip_sig_prefix(auth_name): + """Normalize auth type names by removing any 'sig' prefix. + Mirrors EndpointRulesetResolver._strip_sig_prefix in regions.py. + """ + return auth_name[3:] if auth_name.startswith('sig') else auth_name + + +def _resolve_auth_scheme_override(context, signing_name): + """Return a signature version override if the user has configured one, + otherwise return None to proceed with the endpoint-ruleset-chosen scheme. + + Respects two configuration mechanisms in priority order: + 1. ``signature_version`` set in code (ClientConfigString) — direct + override; suppresses auth_scheme_preference. + 2. ``auth_scheme_preference`` — a priority-ordered list reprioritized + against the endpoint's supported schemes; the first supported scheme + wins. Unsupported schemes in the preference list are ignored. + + The supported schemes follow the resolution hierarchy: + 1. Endpoints 2.0 authSchemes (ruleset) — most specific, used when present + 2. Service trait auth_options (service-2.json metadata) — fallback + """ + client_config = context.get('client_config') + if client_config is None: + return None + + signature_version = client_config.signature_version + auth_scheme_preference = client_config.auth_scheme_preference + + # If signature_version was explicitly set in code, use it as a direct + # override and do not consult auth_scheme_preference. + if isinstance(signature_version, ClientConfigString): + resolved = signature_version + if signing_name in S3_SIGNING_NAMES and not resolved.startswith('s3'): + resolved = f's3{resolved}' + return resolved + + # auth_scheme_preference: reprioritize the endpoint's supported schemes + # based on the user's preference list, ignoring unsupported schemes. + # Candidates follow the resolution hierarchy: ruleset authSchemes when + # present (endpoints 2.0), falling back to service trait auth_options. + if not auth_scheme_preference: + return None + ruleset_schemes = [ + s['name'] + for s in context.get('endpoint_properties', {}).get('authSchemes', []) + ] + candidates = ruleset_schemes or context.get('auth_options') or [] + if not candidates: + return None + preferred_schemes = auth_scheme_preference.split(',') + try: + resolved = resolve_auth_scheme_preference( + preferred_schemes, candidates + ) + except UnsupportedSignatureVersionError: + return None + if resolved == 'v4a': + # Preference resolves to the same scheme already chosen; no override. + return None + sig_version = botocore.UNSIGNED if resolved == 'none' else resolved + if ( + sig_version is not botocore.UNSIGNED + and signing_name in S3_SIGNING_NAMES + and not sig_version.startswith('s3') + ): + sig_version = f's3{sig_version}' + + # Re-apply the signing context from the chosen scheme's ruleset entry, + # replacing the sigv4a context (region='*') that was written during + # endpoint resolution. This ensures signingRegion, signingName, and + # disableDoubleEncoding are correct for the overridden scheme. + ruleset_auth_schemes = ( + context.get('endpoint_properties', {}).get('authSchemes', []) + ) + chosen_scheme = next( + ( + s for s in ruleset_auth_schemes + if _strip_sig_prefix(s.get('name', '')) == resolved + ), + None, + ) + if chosen_scheme is not None: + signing_context = {} + if 'signingRegion' in chosen_scheme: + signing_context['region'] = chosen_scheme['signingRegion'] + elif 'signingRegionSet' in chosen_scheme: + signing_context['region'] = ','.join( + chosen_scheme['signingRegionSet'] + ) + if 'signingName' in chosen_scheme: + signing_context['signing_name'] = chosen_scheme['signingName'] + if 'disableDoubleEncoding' in chosen_scheme: + signing_context['disableDoubleEncoding'] = ensure_boolean( + chosen_scheme['disableDoubleEncoding'] + ) + if 'signing' in context: + context['signing'].update(signing_context) + else: + context['signing'] = signing_context + + return sig_version + + def _resolve_sigv4a_region(context): region = None if 'client_config' in context: diff --git a/awscli/botocore/signers.py b/awscli/botocore/signers.py index 8cd7b37215fb..d8468ab6ee18 100644 --- a/awscli/botocore/signers.py +++ b/awscli/botocore/signers.py @@ -234,6 +234,8 @@ def _choose_signer(self, operation_name, signing_type, context): context=context, ) + print(f'result: {handler, response}') + if response is not None: signature_version = response # The suffix needs to be checked again in case we get an improper From 5be9c6620ef81ea558222de7477893d4d3a53f35 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 15:36:55 -0400 Subject: [PATCH 02/16] Rename bearer auth function. --- awscli/botocore/handlers.py | 52 ++++++++++--------------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index 74445e1fffd8..e662c20a7ddf 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -161,6 +161,11 @@ def set_operation_specific_signer(context, signing_name, **kwargs): if auth_type == 'bearer': return 'bearer' + # Apply auth_scheme_preference override before committing to any signer. + override = _resolve_auth_scheme_override(context, signing_name) + if override is not None: + return override + # If the operation needs an unsigned body, we set additional context # allowing the signer to be aware of this. if context.get('unsigned_payload') or auth_type == 'v4-unsigned-body': @@ -171,13 +176,6 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return auth_type if auth_type == 'v4a': - # Before committing to sigv4a, check if the user has configured - # an auth scheme override. This must be done here because - # emit_until_response stops after this function returns non-None, - # preventing _set_auth_scheme_preference_signer from running. - override = _resolve_auth_scheme_override(context, signing_name) - if override is not None: - return override # If sigv4a is chosen, we must add additional signing config for # global signature. region = _resolve_sigv4a_region(context) @@ -202,7 +200,6 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return signature_version - def _strip_sig_prefix(auth_name): """Normalize auth type names by removing any 'sig' prefix. Mirrors EndpointRulesetResolver._strip_sig_prefix in regions.py. @@ -260,7 +257,7 @@ def _resolve_auth_scheme_override(context, signing_name): ) except UnsupportedSignatureVersionError: return None - if resolved == 'v4a': + if resolved == _strip_sig_prefix(context.get('auth_type', '')): # Preference resolves to the same scheme already chosen; no override. return None sig_version = botocore.UNSIGNED if resolved == 'none' else resolved @@ -1321,10 +1318,10 @@ def _handle_request_validation_mode_member(params, model, **kwargs): params.setdefault(mode_member, "ENABLED") -def _set_auth_scheme_preference_signer(context, signing_name, **kwargs): +def _prefer_bearer_auth_if_available(context, signing_name, **kwargs): """ - Determines the appropriate signer to use based on the client configuration, - authentication scheme preferences, and the availability of a bearer token. + Prefers 'bearer' signature version if a bearer token is available and + allowed for this service, and no explicit auth configuration was set in code. """ client_config = context.get('client_config') if client_config is None: @@ -1345,39 +1342,16 @@ def _set_auth_scheme_preference_signer(context, signing_name, **kwargs): signature_version_set_in_code or auth_preference_set_in_code ) - resolved_signature_version = signature_version - - # If signature version was not set in code, but an auth scheme preference - # is available, resolve it based on the preferred schemes and supported auth - # options for this service. - if ( - not signature_version_set_in_code - and auth_scheme_preference - and auth_options - ): - preferred_schemes = auth_scheme_preference.split(',') - resolved = botocore.auth.resolve_auth_scheme_preference( - preferred_schemes, auth_options - ) - resolved_signature_version = ( - botocore.UNSIGNED if resolved == 'none' else resolved - ) - - # Prefer 'bearer' signature version if a bearer token is available, and it - # is allowed for this service. This can override earlier resolution if the - # config object didn't explicitly set a signature version. if _should_prefer_bearer_auth( has_in_code_configuration, signing_name, - resolved_signature_version, + signature_version, auth_options, ): register_feature_id('BEARER_SERVICE_ENV_VARS') - resolved_signature_version = 'bearer' + return 'bearer' - if resolved_signature_version == signature_version: - return None - return resolved_signature_version + return None def _should_prefer_bearer_auth( @@ -1516,7 +1490,7 @@ def _set_extra_headers_for_unsigned_request( ('choose-signer.sts.AssumeRoleWithSAML', disable_signing), ('choose-signer.sts.AssumeRoleWithWebIdentity', disable_signing), ('choose-signer', set_operation_specific_signer), - ('choose-signer', _set_auth_scheme_preference_signer), + ('choose-signer', _prefer_bearer_auth_if_available), ('before-parameter-build.s3.HeadObject', sse_md5), ('before-parameter-build.s3.GetObject', sse_md5), ('before-parameter-build.s3.PutObject', sse_md5), From e09d885382b2772abab54517a2ea528b0bcf8130 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 15:38:27 -0400 Subject: [PATCH 03/16] Remove debug statement --- awscli/botocore/auth.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awscli/botocore/auth.py b/awscli/botocore/auth.py index cfd001ad9cfb..9c620a560894 100644 --- a/awscli/botocore/auth.py +++ b/awscli/botocore/auth.py @@ -915,8 +915,6 @@ def resolve_auth_scheme_preference(preference_list, auth_options): if scheme in service_supported ] - print(f'prioritized: {prioritized_schemes}') - for scheme in prioritized_schemes: if scheme == 'noAuth': return AUTH_PREF_TO_SIGNATURE_VERSION[scheme] From 77e7f7cb6bef4829815e87f8cb0d849011ef1572 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 15:38:51 -0400 Subject: [PATCH 04/16] Remove unused print statement. --- awscli/botocore/auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awscli/botocore/auth.py b/awscli/botocore/auth.py index 9c620a560894..8ceb4ff7dcbb 100644 --- a/awscli/botocore/auth.py +++ b/awscli/botocore/auth.py @@ -920,7 +920,6 @@ def resolve_auth_scheme_preference(preference_list, auth_options): return AUTH_PREF_TO_SIGNATURE_VERSION[scheme] sig_version = AUTH_PREF_TO_SIGNATURE_VERSION.get(scheme) if sig_version in AUTH_TYPE_MAPS: - print(f'sig version: {sig_version}') return sig_version raise UnsupportedSignatureVersionError( From 24c9076590f724458b2bc683ff587c248a0d522d Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 15:47:22 -0400 Subject: [PATCH 05/16] MAke sure operation-level auth is in candidates list. --- awscli/botocore/client.py | 2 +- awscli/botocore/handlers.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/awscli/botocore/client.py b/awscli/botocore/client.py index b04b3f5c70c5..64a41c6f7ffd 100644 --- a/awscli/botocore/client.py +++ b/awscli/botocore/client.py @@ -844,7 +844,7 @@ def _make_api_call(self, operation_name, api_params): 'has_streaming_input': operation_model.has_streaming_input, 'auth_type': operation_model.resolved_auth_type, 'unsigned_payload': operation_model.unsigned_payload, - 'auth_options': self._service_model.metadata.get('auth'), + 'auth_options': operation_model.auth or self._service_model.metadata.get('auth'), } api_params = self._emit_api_params( diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index e662c20a7ddf..2efba2205a45 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -239,8 +239,10 @@ def _resolve_auth_scheme_override(context, signing_name): # auth_scheme_preference: reprioritize the endpoint's supported schemes # based on the user's preference list, ignoring unsupported schemes. - # Candidates follow the resolution hierarchy: ruleset authSchemes when - # present (endpoints 2.0), falling back to service trait auth_options. + # Candidates follow the resolution hierarchy: + # 1. Ruleset authSchemes (endpoints 2.0) — when present + # 2. Operation auth trait — when present (via auth_options) + # 3. Service auth trait — fallback (via auth_options) if not auth_scheme_preference: return None ruleset_schemes = [ From b5e9d0b028126f0116e0c11e160129a8314655d2 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 15:48:20 -0400 Subject: [PATCH 06/16] Remove debug print. --- awscli/botocore/signers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awscli/botocore/signers.py b/awscli/botocore/signers.py index d8468ab6ee18..8cd7b37215fb 100644 --- a/awscli/botocore/signers.py +++ b/awscli/botocore/signers.py @@ -234,8 +234,6 @@ def _choose_signer(self, operation_name, signing_type, context): context=context, ) - print(f'result: {handler, response}') - if response is not None: signature_version = response # The suffix needs to be checked again in case we get an improper From 7a4c2700463a1fbc12e4cf800342653806f9cf22 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 16:12:51 -0400 Subject: [PATCH 07/16] Extract common code. --- awscli/botocore/handlers.py | 19 ++++-------------- awscli/botocore/regions.py | 40 ++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index 2efba2205a45..5cb2281368de 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -53,7 +53,7 @@ UnsupportedSignatureVersionError, UnsupportedTLSVersionWarning, ) -from botocore.regions import EndpointResolverBuiltins +from botocore.regions import EndpointResolverBuiltins, build_signing_context_from_ruleset_scheme from botocore.signers import ( add_dsql_generate_db_auth_token_methods, add_generate_db_auth_token, @@ -65,7 +65,6 @@ SAFE_CHARS, SERVICE_NAME_ALIASES, ArnParser, - ensure_boolean, get_token_from_environment, hyphenize_service_id, # noqa is_global_accesspoint, # noqa @@ -285,19 +284,9 @@ def _resolve_auth_scheme_override(context, signing_name): None, ) if chosen_scheme is not None: - signing_context = {} - if 'signingRegion' in chosen_scheme: - signing_context['region'] = chosen_scheme['signingRegion'] - elif 'signingRegionSet' in chosen_scheme: - signing_context['region'] = ','.join( - chosen_scheme['signingRegionSet'] - ) - if 'signingName' in chosen_scheme: - signing_context['signing_name'] = chosen_scheme['signingName'] - if 'disableDoubleEncoding' in chosen_scheme: - signing_context['disableDoubleEncoding'] = ensure_boolean( - chosen_scheme['disableDoubleEncoding'] - ) + signing_context = build_signing_context_from_ruleset_scheme( + chosen_scheme + ) if 'signing' in context: context['signing'].update(signing_context) else: diff --git a/awscli/botocore/regions.py b/awscli/botocore/regions.py index 32367ef8dcc3..0505a8061ceb 100644 --- a/awscli/botocore/regions.py +++ b/awscli/botocore/regions.py @@ -49,6 +49,31 @@ DEFAULT_SERVICE_DATA = {'endpoints': {}} +def build_signing_context_from_ruleset_scheme(scheme): + """Build a signing context dict from a single authSchemes entry. + + :type scheme: dict + :param scheme: A single entry from an endpoint ruleset's ``authSchemes`` + list, with the ``sig`` prefix already stripped from ``name`` if needed. + + :rtype: dict + :return: Signing context dict for use in ``request_context['signing']``. + """ + signing_context = {} + if 'signingRegion' in scheme: + signing_context['region'] = scheme['signingRegion'] + elif 'signingRegionSet' in scheme: + if len(scheme['signingRegionSet']) > 0: + signing_context['region'] = ','.join(scheme['signingRegionSet']) + if 'signingName' in scheme: + signing_context['signing_name'] = scheme['signingName'] + if 'disableDoubleEncoding' in scheme: + signing_context['disableDoubleEncoding'] = ensure_boolean( + scheme['disableDoubleEncoding'] + ) + return signing_context + + class BaseEndpointResolver: """Resolves regions and endpoints. Must be subclassed.""" @@ -730,20 +755,7 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): signature_version=', '.join(auth_type_options) ) - signing_context = {} - if 'signingRegion' in scheme: - signing_context['region'] = scheme['signingRegion'] - elif 'signingRegionSet' in scheme: - if len(scheme['signingRegionSet']) > 0: - signing_context['region'] = ','.join( - scheme['signingRegionSet'] - ) - if 'signingName' in scheme: - signing_context.update(signing_name=scheme['signingName']) - if 'disableDoubleEncoding' in scheme: - signing_context['disableDoubleEncoding'] = ensure_boolean( - scheme['disableDoubleEncoding'] - ) + signing_context = build_signing_context_from_ruleset_scheme(scheme) LOG.debug( 'Selected auth type "%s" as "%s" with signing context params: %s', From 9a8cd56b55cfe32fd01bf0a4764a4b0a81c6db10 Mon Sep 17 00:00:00 2001 From: aemous Date: Fri, 27 Mar 2026 16:27:29 -0400 Subject: [PATCH 08/16] Remove UnsupportedSignatureVersionError try-except clause; was not there before. --- awscli/botocore/handlers.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index 5cb2281368de..bbef240be361 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -50,7 +50,6 @@ AliasConflictParameterError, MissingServiceIdError, # noqa ParamValidationError, - UnsupportedSignatureVersionError, UnsupportedTLSVersionWarning, ) from botocore.regions import EndpointResolverBuiltins, build_signing_context_from_ruleset_scheme @@ -252,12 +251,9 @@ def _resolve_auth_scheme_override(context, signing_name): if not candidates: return None preferred_schemes = auth_scheme_preference.split(',') - try: - resolved = resolve_auth_scheme_preference( - preferred_schemes, candidates - ) - except UnsupportedSignatureVersionError: - return None + resolved = resolve_auth_scheme_preference( + preferred_schemes, candidates + ) if resolved == _strip_sig_prefix(context.get('auth_type', '')): # Preference resolves to the same scheme already chosen; no override. return None From 0563fe86b1a38fc2f326000c305fb1257dede3b9 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 12:13:17 -0400 Subject: [PATCH 09/16] Update code architecture to be cleaner, based on feedback. --- awscli/botocore/args.py | 4 + awscli/botocore/client.py | 18 +++- awscli/botocore/handlers.py | 161 +++++++++++------------------------- awscli/botocore/regions.py | 22 +++++ 4 files changed, 89 insertions(+), 116 deletions(-) diff --git a/awscli/botocore/args.py b/awscli/botocore/args.py index 422e7492f121..c3483e4b19b1 100644 --- a/awscli/botocore/args.py +++ b/awscli/botocore/args.py @@ -116,6 +116,7 @@ def get_client_args( signing_region = endpoint_config['signing_region'] endpoint_region_name = endpoint_config['region_name'] account_id_endpoint_mode = config_kwargs['account_id_endpoint_mode'] + preferred_auth_schemes = config_kwargs['auth_scheme_preference'] event_emitter = copy.copy(self._event_emitter) signer = RequestSigner( @@ -165,6 +166,7 @@ def get_client_args( event_emitter, credentials, account_id_endpoint_mode, + preferred_auth_schemes, ) # Copy the session's user agent factory and adds client configuration. @@ -550,6 +552,7 @@ def _build_endpoint_resolver( event_emitter, credentials, account_id_endpoint_mode, + preferred_auth_schemes, ): if endpoints_ruleset_data is None: return None @@ -599,6 +602,7 @@ def _build_endpoint_resolver( event_emitter=event_emitter, use_ssl=is_secure, requested_auth_scheme=sig_version, + preferred_auth_schemes=preferred_auth_schemes, ) def compute_endpoint_resolver_builtin_defaults( diff --git a/awscli/botocore/client.py b/awscli/botocore/client.py index 64a41c6f7ffd..67389c5a6c4a 100644 --- a/awscli/botocore/client.py +++ b/awscli/botocore/client.py @@ -62,6 +62,8 @@ get_service_module_name, ) +from awscli.botocore.auth import resolve_auth_scheme_preference + logger = logging.getLogger(__name__) history_recorder = get_global_history_recorder() @@ -838,13 +840,25 @@ def _make_api_call(self, operation_name, api_params): logger.debug( 'Warning: %s.%s() is deprecated', service_name, operation_name ) + # If the operation has the `auth` property and the client has a + # configured auth scheme preference, use both to compute the + # auth type. Otherwise, fallback to auth/auth_type resolution. + if operation_model.auth and self.meta.config.auth_scheme_preference: + preferred_schemes = ( + self.meta.config.auth_scheme_preference.split(',') + ) + auth_type = resolve_auth_scheme_preference( + preferred_schemes, operation_model.auth + ) + else: + auth_type = operation_model.resolved_auth_type request_context = { 'client_region': self.meta.region_name, 'client_config': self.meta.config, 'has_streaming_input': operation_model.has_streaming_input, - 'auth_type': operation_model.resolved_auth_type, + 'auth_type': auth_type, 'unsigned_payload': operation_model.unsigned_payload, - 'auth_options': operation_model.auth or self._service_model.metadata.get('auth'), + 'auth_options': self._service_model.metadata.get('auth'), } api_params = self._emit_api_params( diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index bbef240be361..053ed14ad7d3 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -159,11 +159,6 @@ def set_operation_specific_signer(context, signing_name, **kwargs): if auth_type == 'bearer': return 'bearer' - # Apply auth_scheme_preference override before committing to any signer. - override = _resolve_auth_scheme_override(context, signing_name) - if override is not None: - return override - # If the operation needs an unsigned body, we set additional context # allowing the signer to be aware of this. if context.get('unsigned_payload') or auth_type == 'v4-unsigned-body': @@ -205,90 +200,64 @@ def _strip_sig_prefix(auth_name): return auth_name[3:] if auth_name.startswith('sig') else auth_name -def _resolve_auth_scheme_override(context, signing_name): - """Return a signature version override if the user has configured one, - otherwise return None to proceed with the endpoint-ruleset-chosen scheme. - - Respects two configuration mechanisms in priority order: - 1. ``signature_version`` set in code (ClientConfigString) — direct - override; suppresses auth_scheme_preference. - 2. ``auth_scheme_preference`` — a priority-ordered list reprioritized - against the endpoint's supported schemes; the first supported scheme - wins. Unsupported schemes in the preference list are ignored. - - The supported schemes follow the resolution hierarchy: - 1. Endpoints 2.0 authSchemes (ruleset) — most specific, used when present - 2. Service trait auth_options (service-2.json metadata) — fallback +def _set_auth_scheme_preference_signer(context, signing_name, **kwargs): + """ + Determines the appropriate signer to use based on the client configuration, + authentication scheme preferences, and the availability of a bearer token. """ client_config = context.get('client_config') if client_config is None: - return None + return signature_version = client_config.signature_version auth_scheme_preference = client_config.auth_scheme_preference + auth_options = context.get('auth_options') - # If signature_version was explicitly set in code, use it as a direct - # override and do not consult auth_scheme_preference. - if isinstance(signature_version, ClientConfigString): - resolved = signature_version - if signing_name in S3_SIGNING_NAMES and not resolved.startswith('s3'): - resolved = f's3{resolved}' - return resolved - - # auth_scheme_preference: reprioritize the endpoint's supported schemes - # based on the user's preference list, ignoring unsupported schemes. - # Candidates follow the resolution hierarchy: - # 1. Ruleset authSchemes (endpoints 2.0) — when present - # 2. Operation auth trait — when present (via auth_options) - # 3. Service auth trait — fallback (via auth_options) - if not auth_scheme_preference: - return None - ruleset_schemes = [ - s['name'] - for s in context.get('endpoint_properties', {}).get('authSchemes', []) - ] - candidates = ruleset_schemes or context.get('auth_options') or [] - if not candidates: - return None - preferred_schemes = auth_scheme_preference.split(',') - resolved = resolve_auth_scheme_preference( - preferred_schemes, candidates + signature_version_set_in_code = ( + isinstance(signature_version, ClientConfigString) + or signature_version is botocore.UNSIGNED ) - if resolved == _strip_sig_prefix(context.get('auth_type', '')): - # Preference resolves to the same scheme already chosen; no override. - return None - sig_version = botocore.UNSIGNED if resolved == 'none' else resolved - if ( - sig_version is not botocore.UNSIGNED - and signing_name in S3_SIGNING_NAMES - and not sig_version.startswith('s3') - ): - sig_version = f's3{sig_version}' - - # Re-apply the signing context from the chosen scheme's ruleset entry, - # replacing the sigv4a context (region='*') that was written during - # endpoint resolution. This ensures signingRegion, signingName, and - # disableDoubleEncoding are correct for the overridden scheme. - ruleset_auth_schemes = ( - context.get('endpoint_properties', {}).get('authSchemes', []) + auth_preference_set_in_code = isinstance( + auth_scheme_preference, ClientConfigString ) - chosen_scheme = next( - ( - s for s in ruleset_auth_schemes - if _strip_sig_prefix(s.get('name', '')) == resolved - ), - None, + has_in_code_configuration = ( + signature_version_set_in_code or auth_preference_set_in_code ) - if chosen_scheme is not None: - signing_context = build_signing_context_from_ruleset_scheme( - chosen_scheme + + resolved_signature_version = signature_version + + # If signature version was not set in code, but an auth scheme preference + # is available, resolve it based on the preferred schemes and supported auth + # options for this service. + if ( + not signature_version_set_in_code + and auth_scheme_preference + and auth_options + ): + preferred_schemes = auth_scheme_preference.split(',') + resolved = botocore.auth.resolve_auth_scheme_preference( + preferred_schemes, auth_options + ) + print(f'preferred: {preferred_schemes}, auth options: {auth_options}, resolved: {resolved}') + resolved_signature_version = ( + botocore.UNSIGNED if resolved == 'none' else resolved ) - if 'signing' in context: - context['signing'].update(signing_context) - else: - context['signing'] = signing_context - return sig_version + # Prefer 'bearer' signature version if a bearer token is available, and it + # is allowed for this service. This can override earlier resolution if the + # config object didn't explicitly set a signature version. + if _should_prefer_bearer_auth( + has_in_code_configuration, + signing_name, + resolved_signature_version, + auth_options, + ): + register_feature_id('BEARER_SERVICE_ENV_VARS') + resolved_signature_version = 'bearer' + + if resolved_signature_version == signature_version: + return None + return resolved_signature_version def _resolve_sigv4a_region(context): @@ -1305,42 +1274,6 @@ def _handle_request_validation_mode_member(params, model, **kwargs): params.setdefault(mode_member, "ENABLED") -def _prefer_bearer_auth_if_available(context, signing_name, **kwargs): - """ - Prefers 'bearer' signature version if a bearer token is available and - allowed for this service, and no explicit auth configuration was set in code. - """ - client_config = context.get('client_config') - if client_config is None: - return - - signature_version = client_config.signature_version - auth_scheme_preference = client_config.auth_scheme_preference - auth_options = context.get('auth_options') - - signature_version_set_in_code = ( - isinstance(signature_version, ClientConfigString) - or signature_version is botocore.UNSIGNED - ) - auth_preference_set_in_code = isinstance( - auth_scheme_preference, ClientConfigString - ) - has_in_code_configuration = ( - signature_version_set_in_code or auth_preference_set_in_code - ) - - if _should_prefer_bearer_auth( - has_in_code_configuration, - signing_name, - signature_version, - auth_options, - ): - register_feature_id('BEARER_SERVICE_ENV_VARS') - return 'bearer' - - return None - - def _should_prefer_bearer_auth( has_in_code_configuration, signing_name, @@ -1477,7 +1410,7 @@ def _set_extra_headers_for_unsigned_request( ('choose-signer.sts.AssumeRoleWithSAML', disable_signing), ('choose-signer.sts.AssumeRoleWithWebIdentity', disable_signing), ('choose-signer', set_operation_specific_signer), - ('choose-signer', _prefer_bearer_auth_if_available), + ('choose-signer', _set_auth_scheme_preference_signer), ('before-parameter-build.s3.HeadObject', sse_md5), ('before-parameter-build.s3.GetObject', sse_md5), ('before-parameter-build.s3.PutObject', sse_md5), diff --git a/awscli/botocore/regions.py b/awscli/botocore/regions.py index 0505a8061ceb..3dbcc687cbbd 100644 --- a/awscli/botocore/regions.py +++ b/awscli/botocore/regions.py @@ -491,6 +491,7 @@ def __init__( event_emitter, use_ssl=True, requested_auth_scheme=None, + preferred_auth_schemes=None, ): self._provider = EndpointProvider( ruleset_data=endpoint_ruleset_data, @@ -503,6 +504,7 @@ def __init__( self._event_emitter = event_emitter self._use_ssl = use_ssl self._requested_auth_scheme = requested_auth_scheme + self._preferred_auth_schemes = preferred_auth_schemes self._instance_cache = {} def construct_endpoint( @@ -718,6 +720,26 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): if self._requested_auth_scheme == UNSIGNED: return 'none', {} + # if a preferred auth schemes list is provided, reorder the auth schemes + # list based on the preferred ordering. + if self._preferred_auth_schemes is not None: + self._preferred_auth_schemes = ( + self._preferred_auth_schemes.split(',') + ) + new_schemes_list = [] + for preferred_scheme in self._preferred_auth_schemes: + for candidate_scheme in auth_schemes: + if preferred_scheme == candidate_scheme['name']: + new_schemes_list.append(candidate_scheme) + + # Add remaining auth schemes that weren't in the preference list. + new_schemes_list.extend([ + s for s in auth_schemes if ( + s['name'] not in self._preferred_auth_schemes + ) + ]) + auth_schemes = new_schemes_list + auth_schemes = [ {**scheme, 'name': self._strip_sig_prefix(scheme['name'])} for scheme in auth_schemes From 8daf219dff8a1946ecbf782517c9d835650e6e5a Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 12:15:28 -0400 Subject: [PATCH 10/16] Remove dead code. --- awscli/botocore/handlers.py | 6 ------ awscli/botocore/regions.py | 40 +++++++++++++------------------------ 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index 053ed14ad7d3..b6186bc4af4e 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -193,12 +193,6 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return signature_version -def _strip_sig_prefix(auth_name): - """Normalize auth type names by removing any 'sig' prefix. - Mirrors EndpointRulesetResolver._strip_sig_prefix in regions.py. - """ - return auth_name[3:] if auth_name.startswith('sig') else auth_name - def _set_auth_scheme_preference_signer(context, signing_name, **kwargs): """ diff --git a/awscli/botocore/regions.py b/awscli/botocore/regions.py index 3dbcc687cbbd..bce2e8321a90 100644 --- a/awscli/botocore/regions.py +++ b/awscli/botocore/regions.py @@ -49,31 +49,6 @@ DEFAULT_SERVICE_DATA = {'endpoints': {}} -def build_signing_context_from_ruleset_scheme(scheme): - """Build a signing context dict from a single authSchemes entry. - - :type scheme: dict - :param scheme: A single entry from an endpoint ruleset's ``authSchemes`` - list, with the ``sig`` prefix already stripped from ``name`` if needed. - - :rtype: dict - :return: Signing context dict for use in ``request_context['signing']``. - """ - signing_context = {} - if 'signingRegion' in scheme: - signing_context['region'] = scheme['signingRegion'] - elif 'signingRegionSet' in scheme: - if len(scheme['signingRegionSet']) > 0: - signing_context['region'] = ','.join(scheme['signingRegionSet']) - if 'signingName' in scheme: - signing_context['signing_name'] = scheme['signingName'] - if 'disableDoubleEncoding' in scheme: - signing_context['disableDoubleEncoding'] = ensure_boolean( - scheme['disableDoubleEncoding'] - ) - return signing_context - - class BaseEndpointResolver: """Resolves regions and endpoints. Must be subclassed.""" @@ -777,7 +752,20 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): signature_version=', '.join(auth_type_options) ) - signing_context = build_signing_context_from_ruleset_scheme(scheme) + signing_context = {} + if 'signingRegion' in scheme: + signing_context['region'] = scheme['signingRegion'] + elif 'signingRegionSet' in scheme: + if len(scheme['signingRegionSet']) > 0: + signing_context['region'] = ','.join( + scheme['signingRegionSet'] + ) + if 'signingName' in scheme: + signing_context.update(signing_name=scheme['signingName']) + if 'disableDoubleEncoding' in scheme: + signing_context['disableDoubleEncoding'] = ensure_boolean( + scheme['disableDoubleEncoding'] + ) LOG.debug( 'Selected auth type "%s" as "%s" with signing context params: %s', From 5e9e1a2f881640d7b9258f0de5c9849c7586a2f7 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 12:17:04 -0400 Subject: [PATCH 11/16] Migrate import to botocore from awscli. --- awscli/botocore/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/awscli/botocore/client.py b/awscli/botocore/client.py index 67389c5a6c4a..d41e0bafdb5a 100644 --- a/awscli/botocore/client.py +++ b/awscli/botocore/client.py @@ -19,7 +19,11 @@ xform_name, ) from botocore.args import ClientArgsCreator -from botocore.auth import AUTH_TYPE_MAPS, resolve_auth_type +from botocore.auth import ( + AUTH_TYPE_MAPS, + resolve_auth_scheme_preference, + resolve_auth_type, +) from botocore.awsrequest import prepare_request_dict from botocore.compress import maybe_compress_request @@ -62,8 +66,6 @@ get_service_module_name, ) -from awscli.botocore.auth import resolve_auth_scheme_preference - logger = logging.getLogger(__name__) history_recorder = get_global_history_recorder() From 9847e4e8da6c3067e9fae5c722f801c4fe02b357 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 12:20:09 -0400 Subject: [PATCH 12/16] Move the function to the right location. --- awscli/botocore/handlers.py | 121 ++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index b6186bc4af4e..d363e9c02058 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -52,7 +52,7 @@ ParamValidationError, UnsupportedTLSVersionWarning, ) -from botocore.regions import EndpointResolverBuiltins, build_signing_context_from_ruleset_scheme +from botocore.regions import EndpointResolverBuiltins from botocore.signers import ( add_dsql_generate_db_auth_token_methods, add_generate_db_auth_token, @@ -194,66 +194,6 @@ def set_operation_specific_signer(context, signing_name, **kwargs): return signature_version -def _set_auth_scheme_preference_signer(context, signing_name, **kwargs): - """ - Determines the appropriate signer to use based on the client configuration, - authentication scheme preferences, and the availability of a bearer token. - """ - client_config = context.get('client_config') - if client_config is None: - return - - signature_version = client_config.signature_version - auth_scheme_preference = client_config.auth_scheme_preference - auth_options = context.get('auth_options') - - signature_version_set_in_code = ( - isinstance(signature_version, ClientConfigString) - or signature_version is botocore.UNSIGNED - ) - auth_preference_set_in_code = isinstance( - auth_scheme_preference, ClientConfigString - ) - has_in_code_configuration = ( - signature_version_set_in_code or auth_preference_set_in_code - ) - - resolved_signature_version = signature_version - - # If signature version was not set in code, but an auth scheme preference - # is available, resolve it based on the preferred schemes and supported auth - # options for this service. - if ( - not signature_version_set_in_code - and auth_scheme_preference - and auth_options - ): - preferred_schemes = auth_scheme_preference.split(',') - resolved = botocore.auth.resolve_auth_scheme_preference( - preferred_schemes, auth_options - ) - print(f'preferred: {preferred_schemes}, auth options: {auth_options}, resolved: {resolved}') - resolved_signature_version = ( - botocore.UNSIGNED if resolved == 'none' else resolved - ) - - # Prefer 'bearer' signature version if a bearer token is available, and it - # is allowed for this service. This can override earlier resolution if the - # config object didn't explicitly set a signature version. - if _should_prefer_bearer_auth( - has_in_code_configuration, - signing_name, - resolved_signature_version, - auth_options, - ): - register_feature_id('BEARER_SERVICE_ENV_VARS') - resolved_signature_version = 'bearer' - - if resolved_signature_version == signature_version: - return None - return resolved_signature_version - - def _resolve_sigv4a_region(context): region = None if 'client_config' in context: @@ -1268,6 +1208,65 @@ def _handle_request_validation_mode_member(params, model, **kwargs): params.setdefault(mode_member, "ENABLED") +def _set_auth_scheme_preference_signer(context, signing_name, **kwargs): + """ + Determines the appropriate signer to use based on the client configuration, + authentication scheme preferences, and the availability of a bearer token. + """ + client_config = context.get('client_config') + if client_config is None: + return + + signature_version = client_config.signature_version + auth_scheme_preference = client_config.auth_scheme_preference + auth_options = context.get('auth_options') + + signature_version_set_in_code = ( + isinstance(signature_version, ClientConfigString) + or signature_version is botocore.UNSIGNED + ) + auth_preference_set_in_code = isinstance( + auth_scheme_preference, ClientConfigString + ) + has_in_code_configuration = ( + signature_version_set_in_code or auth_preference_set_in_code + ) + + resolved_signature_version = signature_version + + # If signature version was not set in code, but an auth scheme preference + # is available, resolve it based on the preferred schemes and supported auth + # options for this service. + if ( + not signature_version_set_in_code + and auth_scheme_preference + and auth_options + ): + preferred_schemes = auth_scheme_preference.split(',') + resolved = botocore.auth.resolve_auth_scheme_preference( + preferred_schemes, auth_options + ) + resolved_signature_version = ( + botocore.UNSIGNED if resolved == 'none' else resolved + ) + + # Prefer 'bearer' signature version if a bearer token is available, and it + # is allowed for this service. This can override earlier resolution if the + # config object didn't explicitly set a signature version. + if _should_prefer_bearer_auth( + has_in_code_configuration, + signing_name, + resolved_signature_version, + auth_options, + ): + register_feature_id('BEARER_SERVICE_ENV_VARS') + resolved_signature_version = 'bearer' + + if resolved_signature_version == signature_version: + return None + return resolved_signature_version + + def _should_prefer_bearer_auth( has_in_code_configuration, signing_name, From cc0548f61f22b075746298e88d01e946d850817f Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 12:24:54 -0400 Subject: [PATCH 13/16] Simplify reordering code added to EndpointRulesetResolver. --- awscli/botocore/regions.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/awscli/botocore/regions.py b/awscli/botocore/regions.py index bce2e8321a90..7bad3b053b1a 100644 --- a/awscli/botocore/regions.py +++ b/awscli/botocore/regions.py @@ -698,22 +698,13 @@ def auth_schemes_to_signing_ctx(self, auth_schemes): # if a preferred auth schemes list is provided, reorder the auth schemes # list based on the preferred ordering. if self._preferred_auth_schemes is not None: - self._preferred_auth_schemes = ( - self._preferred_auth_schemes.split(',') - ) - new_schemes_list = [] - for preferred_scheme in self._preferred_auth_schemes: - for candidate_scheme in auth_schemes: - if preferred_scheme == candidate_scheme['name']: - new_schemes_list.append(candidate_scheme) - - # Add remaining auth schemes that weren't in the preference list. - new_schemes_list.extend([ - s for s in auth_schemes if ( - s['name'] not in self._preferred_auth_schemes - ) - ]) - auth_schemes = new_schemes_list + prefs = self._preferred_auth_schemes.split(',') + by_name = {s['name']: s for s in auth_schemes} + auth_schemes = [ + by_name[p] for p in prefs if p in by_name + ] + [ + s for s in auth_schemes if s['name'] not in prefs + ] auth_schemes = [ {**scheme, 'name': self._strip_sig_prefix(scheme['name'])} From edc7fd565de061709511505de48f0a4e85666cb6 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 12:28:45 -0400 Subject: [PATCH 14/16] Add changelog entry. --- .changes/next-release/bugfix-signing-24412.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/next-release/bugfix-signing-24412.json diff --git a/.changes/next-release/bugfix-signing-24412.json b/.changes/next-release/bugfix-signing-24412.json new file mode 100644 index 000000000000..33669211567b --- /dev/null +++ b/.changes/next-release/bugfix-signing-24412.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "signing", + "description": "Fix bug so that configured auth scheme preference is used when auth scheme is resolved from endpoints rulesets, or from operation-level auth trait. Auth scheme preference can be configured using the existing ``auth_scheme_preference`` shared config setting, or the existing ``AWS_AUTH_SCHEME_PREFERENCE`` environment variable." +} From 5681b21034031d3794d2b568949161f54d4c4df7 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 1 Apr 2026 14:34:59 -0400 Subject: [PATCH 15/16] Add unit test cases for EP2.0 auth_scheme_preference. --- tests/unit/botocore/test_endpoint_provider.py | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/tests/unit/botocore/test_endpoint_provider.py b/tests/unit/botocore/test_endpoint_provider.py index 747264aabb8f..41010c7c8392 100644 --- a/tests/unit/botocore/test_endpoint_provider.py +++ b/tests/unit/botocore/test_endpoint_provider.py @@ -80,6 +80,28 @@ ], }, } +ENDPOINT_AUTH_SCHEMES_DICT = { + "url": URL_TEMPLATE, + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": True, + "name": "foo", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": True, + "name": "bar", + "signingName": "s3-outposts", + "signingRegion": REGION_TEMPLATE, + }, + ], + }, + "headers": {}, +} @pytest.fixture(scope="module") @@ -562,4 +584,71 @@ def test_construct_endpoint_parametrized( resolver, '_get_provider_params', return_value=provider_params ): result = resolver.construct_endpoint(None, None, None) - assert result.url == expected_url \ No newline at end of file + assert result.url == expected_url + + +@pytest.mark.parametrize( + "preferred_auth_schemes,expected_auth_scheme_name", + [ + ( + 'foo,bar', + 'foo', + ), + ( + 'bar,foo', + 'bar', + ), + ( + 'xyz,foo,bar', + 'foo', + ), + ], +) +def test_auth_scheme_preference( + preferred_auth_schemes, + expected_auth_scheme_name, + monkeypatch +): + conditions = [ + PARSE_ARN_FUNC, + { + "fn": "not", + "argv": [STRING_EQUALS_FUNC], + }, + { + "fn": "aws.partition", + "argv": [REGION_REF], + "assign": "PartitionResults", + }, + ], + resolver = EndpointRulesetResolver( + endpoint_ruleset_data={ + 'version': '1.0', + 'parameters': {}, + 'rules': [ + { + 'conditions': conditions, + 'type': 'endpoint', + 'endpoint': ENDPOINT_AUTH_SCHEMES_DICT, + } + ], + }, + partition_data={}, + service_model=None, + builtins={}, + client_context=None, + event_emitter=None, + use_ssl=True, + requested_auth_scheme=None, + preferred_auth_schemes=preferred_auth_schemes, + ) + monkeypatch.setattr( + 'botocore.regions.AUTH_TYPE_MAPS', + {'bar': None, 'foo': None} + ) + auth_schemes = [ + {'name': 'foo', 'signingName': 's3', 'signingRegion': 'ap-south-1'}, + {'name': 'bar', 'signingName': 's3', 'signingRegion': 'ap-south-2'}, + ] + name, scheme = resolver.auth_schemes_to_signing_ctx(auth_schemes) + assert name == expected_auth_scheme_name From cd01f69d5ef9e4381609e6b10cd7d24f231f5ad9 Mon Sep 17 00:00:00 2001 From: aemous Date: Mon, 6 Apr 2026 12:41:13 -0400 Subject: [PATCH 16/16] Remove unused import. --- awscli/botocore/handlers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awscli/botocore/handlers.py b/awscli/botocore/handlers.py index 2678f56e24a6..460e9522e94e 100644 --- a/awscli/botocore/handlers.py +++ b/awscli/botocore/handlers.py @@ -71,8 +71,6 @@ switch_host_with_param, ) -from awscli.botocore.auth import resolve_auth_scheme_preference - logger = logging.getLogger(__name__) REGISTER_FIRST = object()