Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
527f455
Prototype fix for respecting auth scheme preference.
aemous Mar 27, 2026
5be9c66
Rename bearer auth function.
aemous Mar 27, 2026
e09d885
Remove debug statement
aemous Mar 27, 2026
77e7f7c
Remove unused print statement.
aemous Mar 27, 2026
24c9076
MAke sure operation-level auth is in candidates list.
aemous Mar 27, 2026
b5e9d0b
Remove debug print.
aemous Mar 27, 2026
7a4c270
Extract common code.
aemous Mar 27, 2026
9a8cd56
Remove UnsupportedSignatureVersionError try-except clause; was not th…
aemous Mar 27, 2026
0563fe8
Update code architecture to be cleaner, based on feedback.
aemous Apr 1, 2026
8daf219
Remove dead code.
aemous Apr 1, 2026
5e9e1a2
Migrate import to botocore from awscli.
aemous Apr 1, 2026
9847e4e
Move the function to the right location.
aemous Apr 1, 2026
cc0548f
Simplify reordering code added to EndpointRulesetResolver.
aemous Apr 1, 2026
edc7fd5
Add changelog entry.
aemous Apr 1, 2026
a40c847
Merge branch 'v2' into bugfix-auth-scheme-pref
aemous Apr 1, 2026
5681b21
Add unit test cases for EP2.0 auth_scheme_preference.
aemous Apr 1, 2026
cd01f69
Remove unused import.
aemous Apr 6, 2026
fb7201f
Rename local variables based on PR feedback.
aemous Apr 8, 2026
8f3212e
Refactor endpoint preference resolution to use utility function.
aemous Apr 8, 2026
7f9f88b
Remove redundant code.
aemous Apr 8, 2026
ee60b5b
Fix tests based on latest revisions.
aemous Apr 8, 2026
bb313cd
Merge remote-tracking branch 'origin/v2' into bugfix-auth-scheme-pref
aemous Apr 8, 2026
7697542
Clean regions code to use resolve_auth_scheme_preference
aemous Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-signing-24412.json
Original file line number Diff line number Diff line change
@@ -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."
}
4 changes: 4 additions & 0 deletions awscli/botocore/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def get_client_args(
s3_disable_express_session_auth = config_kwargs[
's3_disable_express_session_auth'
]
preferred_auth_schemes = config_kwargs['auth_scheme_preference']
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit, non-blocking]
IMO this variable name should exactly the kwarg name so there's no confusion. There are already a ton of variables around signature version. In this file alone:

signature_version -> sig_version -> requested_auth_scheme, then also you're now adding auth_scheme_preference -> preferred_auth_schemes. It's a lot of complexity to keep in your head, so while it doesn't make a huge difference, I'd prefer to see this renamed to just auth_scheme_preference to make it clear that it's the same thing as the config variable.


event_emitter = copy.copy(self._event_emitter)
signer = RequestSigner(
Expand Down Expand Up @@ -169,6 +170,7 @@ def get_client_args(
credentials,
account_id_endpoint_mode,
s3_disable_express_session_auth,
preferred_auth_schemes,
)

# Copy the session's user agent factory and adds client configuration.
Expand Down Expand Up @@ -589,6 +591,7 @@ def _build_endpoint_resolver(
credentials,
account_id_endpoint_mode,
s3_disable_express_session_auth,
preferred_auth_schemes,
):
if endpoints_ruleset_data is None:
return None
Expand Down Expand Up @@ -645,6 +648,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(
Expand Down
20 changes: 18 additions & 2 deletions awscli/botocore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -838,11 +842,23 @@ 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': self._service_model.metadata.get('auth'),
}
Expand Down
13 changes: 13 additions & 0 deletions awscli/botocore/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ def __init__(
event_emitter,
use_ssl=True,
requested_auth_scheme=None,
preferred_auth_schemes=None,
):
self._provider = EndpointProvider(
ruleset_data=endpoint_ruleset_data,
Expand All @@ -483,6 +484,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(
Expand Down Expand Up @@ -698,6 +700,17 @@ 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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Change requested]
We already have a function called resolve_auth_scheme_preference which handles this and should avoid duplicating preference-resolution / ordering logic here.

Right now we manually reorder the endpoint ruleset authSchemes list based on auth_scheme_preference. Instead, we can reuse resolve_auth_scheme_preference() to pick the desired auth type and then map that back to the actual ruleset scheme dict. Here's the proof of concept I wrote:

        if self._requested_auth_scheme is not None:
             ....
        elif self._preferred_auth_schemes is not None:
            prefs = self._preferred_auth_schemes.split(',')
            available_ruleset_names = [s['name'].split('#')[-1] for s in auth_schemes]
            auth_schemes_by_auth_type = {self._strip_sig_prefix(s['name'].split('#')[-1]): s for s in auth_schemes}
            resolved_auth_type = resolve_auth_scheme_preference(prefs, available_ruleset_names)
            name = resolved_auth_type
            scheme = auth_schemes_by_auth_type[resolved_auth_type]

We could definitely refactor this and/or the resolve_auth_scheme_preference to make it much cleaner. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense. I will revise and go this route.

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'])}
for scheme in auth_schemes
Expand Down
91 changes: 90 additions & 1 deletion tests/unit/botocore/test_endpoint_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
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
Loading