Skip to content

Commit 2c989e2

Browse files
Decouple auth from http
This decouples auth from HTTP, allowing it to be defined centrally. A number of changes have been made to various interfaces. Notably identity resolvers are generally expected to have zero-arg constructors, instead getting everything they need from their properties where possible. Construction of event signers has been moved into AuthScheme. Right now it takes the entire context of the request during construction, but we should consider passing identity and signing properties in at signing time like normal signers so that they can be reused.
1 parent a9d2331 commit 2c989e2

File tree

34 files changed

+741
-588
lines changed

34 files changed

+741
-588
lines changed

packages/aws-sdk-signers/src/aws_sdk_signers/_identity.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,14 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
from dataclasses import dataclass
5-
from datetime import UTC, datetime
5+
from datetime import datetime
66

7-
from .interfaces.identity import Identity
7+
from .interfaces.identity import AWSCredentialsIdentity
88

99

1010
@dataclass(kw_only=True)
11-
class AWSCredentialIdentity(Identity):
11+
class AWSCredentialIdentity(AWSCredentialsIdentity):
1212
access_key_id: str
1313
secret_access_key: str
1414
session_token: str | None = None
1515
expiration: datetime | None = None
16-
17-
@property
18-
def is_expired(self) -> bool:
19-
"""Whether the identity is expired."""
20-
if self.expiration is None:
21-
return False
22-
return self.expiration < datetime.now(UTC)

packages/aws-sdk-signers/src/aws_sdk_signers/interfaces/identity.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,39 @@
33

44
from __future__ import annotations
55

6-
from datetime import datetime
6+
from datetime import UTC, datetime
77
from typing import Protocol, runtime_checkable
88

99

10+
@runtime_checkable
1011
class Identity(Protocol):
1112
"""An entity available to the client representing who the user is."""
1213

13-
# The expiration time of the identity. If time zone is provided,
14-
# it is updated to UTC. The value must always be in UTC.
1514
expiration: datetime | None = None
15+
"""The expiration time of the identity.
16+
17+
If time zone is provided, it is updated to UTC. The value must always be in UTC.
18+
"""
1619

1720
@property
1821
def is_expired(self) -> bool:
1922
"""Whether the identity is expired."""
20-
...
23+
if self.expiration is None:
24+
return False
25+
return datetime.now(tz=UTC) >= self.expiration
2126

2227

2328
@runtime_checkable
24-
class AWSCredentialsIdentity(Protocol):
29+
class AWSCredentialsIdentity(Identity, Protocol):
2530
"""AWS Credentials Identity."""
2631

27-
# The access key ID.
2832
access_key_id: str
33+
"""A unique identifier for an AWS user or role."""
2934

30-
# The secret access key.
3135
secret_access_key: str
36+
"""A secret key used in conjunction with the access key ID to authenticate
37+
programmatic access to AWS services."""
3238

33-
# The session token.
34-
session_token: str | None
35-
36-
expiration: datetime | None = None
37-
38-
@property
39-
def is_expired(self) -> bool:
40-
"""Whether the identity is expired."""
41-
...
39+
session_token: str | None = None
40+
"""A temporary token used to specify the current session for the supplied
41+
credentials."""

packages/aws-sdk-signers/src/aws_sdk_signers/signers.py

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from urllib.parse import parse_qsl, quote
1616

1717
from ._http import AWSRequest, Field, URI
18-
from ._identity import AWSCredentialIdentity
1918
from ._io import AsyncBytesReader
2019
from .exceptions import AWSSDKWarning, MissingExpectedParameterException
2120
from .interfaces.identity import AWSCredentialsIdentity as _AWSCredentialsIdentity
@@ -55,27 +54,27 @@ class SigV4Signer:
5554
def sign(
5655
self,
5756
*,
58-
signing_properties: SigV4SigningProperties,
59-
http_request: AWSRequest,
60-
identity: AWSCredentialIdentity,
57+
request: AWSRequest,
58+
identity: _AWSCredentialsIdentity,
59+
properties: SigV4SigningProperties,
6160
) -> AWSRequest:
6261
"""Generate and apply a SigV4 Signature to a copy of the supplied request.
6362
64-
:param signing_properties: SigV4SigningProperties to define signing primitives
65-
such as the target service, region, and date.
66-
:param http_request: An AWSRequest to sign prior to sending to the service.
63+
:param request: An AWSRequest to sign prior to sending to the service.
6764
:param identity: A set of credentials representing an AWS Identity or role
6865
capacity.
66+
:param properties: SigV4SigningProperties to define signing primitives such as
67+
the target service, region, and date.
6968
"""
7069
# Copy and prepopulate any missing values in the
7170
# supplied request and signing properties.
7271
self._validate_identity(identity=identity)
7372
new_signing_properties = self._normalize_signing_properties(
74-
signing_properties=signing_properties
73+
signing_properties=properties
7574
)
7675
assert "date" in new_signing_properties
7776

78-
new_request = self._generate_new_request(request=http_request)
77+
new_request = self._generate_new_request(request=request)
7978
self._apply_required_fields(
8079
request=new_request,
8180
signing_properties=new_signing_properties,
@@ -164,7 +163,7 @@ def _signature(
164163
def _hash(self, key: bytes, value: str) -> bytes:
165164
return hmac.new(key=key, msg=value.encode(), digestmod=sha256).digest()
166165

167-
def _validate_identity(self, *, identity: AWSCredentialIdentity) -> None:
166+
def _validate_identity(self, *, identity: _AWSCredentialsIdentity) -> None:
168167
"""Perform runtime and expiration checks before attempting signing."""
169168
if not isinstance(identity, _AWSCredentialsIdentity): # pyright: ignore
170169
raise ValueError(
@@ -195,7 +194,7 @@ def _apply_required_fields(
195194
*,
196195
request: AWSRequest,
197196
signing_properties: SigV4SigningProperties,
198-
identity: AWSCredentialIdentity,
197+
identity: _AWSCredentialsIdentity,
199198
) -> None:
200199
# Apply required X-Amz-Date if neither X-Amz-Date nor Date are present.
201200
if "Date" not in request.fields and "X-Amz-Date" not in request.fields:
@@ -427,26 +426,26 @@ class AsyncSigV4Signer:
427426
async def sign(
428427
self,
429428
*,
430-
signing_properties: SigV4SigningProperties,
431-
http_request: AWSRequest,
432-
identity: AWSCredentialIdentity,
429+
request: AWSRequest,
430+
identity: _AWSCredentialsIdentity,
431+
properties: SigV4SigningProperties,
433432
) -> AWSRequest:
434433
"""Generate and apply a SigV4 Signature to a copy of the supplied request.
435434
436-
:param signing_properties: SigV4SigningProperties to define signing primitives
437-
such as the target service, region, and date.
438-
:param http_request: An AWSRequest to sign prior to sending to the service.
435+
:param request: An AWSRequest to sign prior to sending to the service.
439436
:param identity: A set of credentials representing an AWS Identity or role
440437
capacity.
438+
:param properties: SigV4SigningProperties to define signing primitives such as
439+
the target service, region, and date.
441440
"""
442441
# Copy and prepopulate any missing values in the
443442
# supplied request and signing properties.
444443

445444
await self._validate_identity(identity=identity)
446445
new_signing_properties = await self._normalize_signing_properties(
447-
signing_properties=signing_properties
446+
signing_properties=properties
448447
)
449-
new_request = await self._generate_new_request(request=http_request)
448+
new_request = await self._generate_new_request(request=request)
450449
await self._apply_required_fields(
451450
request=new_request,
452451
signing_properties=new_signing_properties,
@@ -455,7 +454,7 @@ async def sign(
455454

456455
# Construct core signing components
457456
canonical_request = await self.canonical_request(
458-
signing_properties=signing_properties,
457+
signing_properties=properties,
459458
request=new_request,
460459
)
461460
string_to_sign = await self.string_to_sign(
@@ -535,7 +534,7 @@ async def _signature(
535534
async def _hash(self, key: bytes, value: str) -> bytes:
536535
return hmac.new(key=key, msg=value.encode(), digestmod=sha256).digest()
537536

538-
async def _validate_identity(self, *, identity: AWSCredentialIdentity) -> None:
537+
async def _validate_identity(self, *, identity: _AWSCredentialsIdentity) -> None:
539538
"""Perform runtime and expiration checks before attempting signing."""
540539
if not isinstance(identity, _AWSCredentialsIdentity): # pyright: ignore
541540
raise ValueError(
@@ -566,7 +565,7 @@ async def _apply_required_fields(
566565
*,
567566
request: AWSRequest,
568567
signing_properties: SigV4SigningProperties,
569-
identity: AWSCredentialIdentity,
568+
identity: _AWSCredentialsIdentity,
570569
) -> None:
571570
# Apply required X-Amz-Date if neither X-Amz-Date nor Date are present.
572571
if "Date" not in request.fields and "X-Amz-Date" not in request.fields:
@@ -804,26 +803,27 @@ class AsyncEventSigner:
804803
def __init__(
805804
self,
806805
*,
807-
signing_properties: SigV4SigningProperties,
808-
identity: AWSCredentialIdentity,
806+
properties: SigV4SigningProperties,
807+
identity: _AWSCredentialsIdentity,
809808
initial_signature: bytes,
809+
event_encoder_cls: type["EventHeaderEncoder"],
810810
):
811-
self._signing_properties = signing_properties
812-
self._identity = identity
813811
self._prior_signature = initial_signature
814812
self._signing_lock = asyncio.Lock()
813+
self._event_encoder_cls = event_encoder_cls
814+
self._properties = properties
815+
self._identity = identity
815816

816-
async def sign_event(
817+
async def sign(
817818
self,
818819
*,
819-
event_message: "EventMessage",
820-
event_encoder_cls: type["EventHeaderEncoder"],
820+
event: "EventMessage",
821821
) -> "EventMessage":
822822
async with self._signing_lock:
823823
# Copy and prepopulate any missing values in the
824824
# signing properties.
825825
new_signing_properties = SigV4SigningProperties( # type: ignore
826-
**self._signing_properties
826+
**self._properties
827827
)
828828
# TODO: If date is in properties, parse a datetime from it.
829829
date_obj = datetime.datetime.now(datetime.UTC)
@@ -834,11 +834,11 @@ async def sign_event(
834834

835835
timestamp = new_signing_properties["date"]
836836
headers: dict[str, str | bytes | datetime.datetime] = {":date": date_obj}
837-
encoder = event_encoder_cls()
837+
encoder = self._event_encoder_cls()
838838
encoder.encode_headers(headers)
839839
encoded_headers = encoder.get_result()
840840

841-
payload = event_message.encode()
841+
payload = event.encode()
842842

843843
string_to_sign = await self._event_string_to_sign(
844844
timestamp=timestamp,
@@ -848,19 +848,20 @@ async def sign_event(
848848
prior_signature=self._prior_signature,
849849
)
850850
event_signature = await self._sign_event(
851+
identity=self._identity,
851852
timestamp=timestamp,
852853
string_to_sign=string_to_sign,
853-
signing_properties=new_signing_properties,
854+
properties=new_signing_properties,
854855
)
855856
headers[":chunk-signature"] = event_signature
856857

857-
event_message.headers = headers
858-
event_message.payload = payload
858+
event.headers = headers
859+
event.payload = payload
859860

860861
# set new prior signature before releasing the lock
861862
self._prior_signature = hexlify(event_signature)
862863

863-
return event_message
864+
return event
864865

865866
async def _event_string_to_sign(
866867
self,
@@ -885,13 +886,14 @@ async def _sign_event(
885886
*,
886887
timestamp: str,
887888
string_to_sign: str,
888-
signing_properties: SigV4SigningProperties,
889+
identity: _AWSCredentialsIdentity,
890+
properties: SigV4SigningProperties,
889891
) -> bytes:
890-
key = self._identity.secret_access_key.encode("utf-8")
892+
key = identity.secret_access_key.encode("utf-8")
891893
today = timestamp[:8].encode("utf-8")
892894
k_date = self._hash(b"AWS4" + key, today)
893-
k_region = self._hash(k_date, signing_properties["region"].encode("utf-8"))
894-
k_service = self._hash(k_region, signing_properties["service"].encode("utf-8"))
895+
k_region = self._hash(k_date, properties["region"].encode("utf-8"))
896+
k_service = self._hash(k_region, properties["service"].encode("utf-8"))
895897
k_signing = self._hash(k_service, b"aws4_request")
896898
return self._hash(k_signing, string_to_sign.encode("utf-8"))
897899

packages/aws-sdk-signers/tests/unit/auth/test_sigv4.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ def _test_signature_version_4_sync(test_case_name: str, signer: SigV4Signer) ->
112112
assert test_case.string_to_sign == actual_string_to_sign
113113
with pytest.warns(AWSSDKWarning):
114114
signed_request = signer.sign(
115-
signing_properties=signing_props,
116-
http_request=request,
115+
properties=signing_props,
116+
request=request,
117117
identity=test_case.credentials,
118118
)
119119
assert (
@@ -151,8 +151,8 @@ async def _test_signature_version_4_async(
151151
assert test_case.string_to_sign == actual_string_to_sign
152152
with pytest.warns(AWSSDKWarning):
153153
signed_request = await signer.sign(
154-
signing_properties=signing_props,
155-
http_request=request,
154+
properties=signing_props,
155+
request=request,
156156
identity=test_case.credentials,
157157
)
158158
assert (

0 commit comments

Comments
 (0)