Skip to content

Commit 7ad2adf

Browse files
Add auth design doc
1 parent 194c334 commit 7ad2adf

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

designs/auth.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Identity and Authentication
2+
3+
Smithy services may define any number of authentication schemes via traits and
4+
configure which schemes are available and prioritized on a per-operation basis.
5+
This document describes how an auth scheme is configured and picked at runtime.
6+
7+
## Auth Schemes
8+
9+
Everything to do with an auth scheme is contained within an implementation of
10+
the `AuthScheme` Protocol. These implementations construct the
11+
[identity resolvers](#identity-resolvers) and [signers](#signers) as well as the
12+
extra properties needed for identity resolution and signing.
13+
14+
Each `AuthScheme` has a `scheme_id`, which is the Smithy shape ID of the auth
15+
scheme.
16+
17+
```python
18+
class AuthScheme[R: Request, I: Identity, IP: Mapping[str, Any], SP: Mapping[str, Any]](
19+
Protocol
20+
):
21+
scheme_id: ShapeID
22+
23+
def identity_properties(self, *, context: _TypedProperties) -> IP:
24+
...
25+
26+
def identity_resolver(
27+
self, *, context: _TypedProperties
28+
) -> IdentityResolver[I, IP]:
29+
...
30+
31+
def signer_properties(self, *, context: _TypedProperties) -> SP:
32+
...
33+
34+
def signer(self) -> Signer[R, I, SP]:
35+
...
36+
37+
def event_signer(self, *, request: R) -> EventSigner[I, SP] | None:
38+
return None
39+
```
40+
41+
`AuthScheme` implementations SHOULD cache identity resolvers and signers if
42+
possible.
43+
44+
### Auth Scheme Resolution
45+
46+
Services and operation may support any number of auth schemes, each of which may
47+
or may not be availble for a number of reasons, such as not being configured. An
48+
`AuthSchemeResolver` is used to figure out which auth scheme to use for each
49+
request.
50+
51+
```python
52+
class AuthSchemeResolver(Protocol):
53+
def resolve_auth_scheme(
54+
self, *, auth_parameters: AuthParams[Any, Any]
55+
) -> Sequence[AuthOption]:
56+
...
57+
58+
class AuthOption(Protocol):
59+
scheme_id: ShapeID
60+
identity_properties: TypedProperties
61+
signer_properties: TypedProperties
62+
63+
@dataclass(kw_only=True, frozen=True)
64+
class AuthParams[I: SerializeableShape, O: DeserializeableShape]:
65+
protocol_id: ShapeID
66+
operation: APIOperation[I, O]
67+
context: TypedProperties
68+
```
69+
70+
The resolver is given the ID of the protocol being used by the client, the
71+
schema of the operation being invoked, and the operation invocation context. It
72+
returns a priority-ordered list of auth schemes to pick from, along with
73+
optional overrides for identity and signer properties.
74+
75+
The client will pick the first auth scheme in the list that has an entry in the
76+
`auth_schemes` [configuration](#configuration) dict and which is able to resolve
77+
an identity.
78+
79+
The resolver itself is stored in the service's [configuration](#configuration)
80+
object, and may be replaced with a custom implemenatation. Default
81+
implementations are generated based on the modeled auth traits.
82+
83+
## Identity
84+
85+
Each auth scheme is associated with an identity type, such as an API key or
86+
username and password. In the AWS context, this is the access key id, secret
87+
access key, and optionally the session token.
88+
89+
Identities MAY be shared between multiple auth schemes. For example, the AWS
90+
sigv4 and sigv4a auth schemes use the same AWS identity.
91+
92+
In Python, each identity type MUST implement the following `Protocol`:
93+
94+
```python
95+
@runtime_checkable
96+
class Identity(Protocol):
97+
98+
expiration: datetime | None = None
99+
100+
@property
101+
def is_expired(self) -> bool:
102+
if self.expiration is None:
103+
return False
104+
return datetime.now(tz=UTC) >= self.expiration
105+
```
106+
107+
An `Identity` may be derived from any number of sources, such as configuration
108+
properties or environement variables. These different sources are loaded by an
109+
[`IdentityResolver`](#identity-resolvers).
110+
111+
### Identity Resolvers
112+
113+
Identity resolvers are responsible for contructiong an `Identity` for a request.
114+
115+
```python
116+
class IdentityResolver[I: Identity, IP: Mapping[str, Any]](Protocol):
117+
118+
async def get_identity(self, *, properties: IP) -> I:
119+
...
120+
```
121+
122+
Each identity source SHOULD have its own identity resolver implementation. If an
123+
`Identity` is supported by multiple `IdentityResolver`s, those resolver SHOULD
124+
be prioritized to provide a stable resolution strategy. A
125+
`ChainedIdentityResolver` implementation is provided that implements this
126+
behavior generically.
127+
128+
The `get_identity` function takes only one (keyword-only) argument - a mapping
129+
of properties that is refined by the `IP` generic parameter. The identity
130+
properties are contructed by the `AuthScheme`'s `identity_properties` method.
131+
132+
Identity resolvers are constructed by the `AuthScheme`'s `identity_resolver`
133+
method.
134+
135+
## Signers
136+
137+
Signers are responsible for signing transport requests so that they can be
138+
authenticated by the server. They are given the transport request to sign, the
139+
resolved identity, and a property mapping that is used for any additional
140+
configuration needed. The signing properties are constructed by the
141+
`AuthScheme`'s `signer_properties` method.
142+
143+
```python
144+
class Signer[R: Request, I, SP: Mapping[str, Any]](Protocol):
145+
async def sign(self, *, request: R, identity: I, properties: SP) -> R:
146+
...
147+
```
148+
149+
Signers are constructed by the `AuthScheme`'s `signer` method.
150+
151+
Signers MAY modify the given request and return it, or construct a new signed
152+
request.
153+
154+
### Event Signers
155+
156+
Auh schemes MAY also have an associated event signer, which signs events that
157+
are sent to a server. They behave in the same way as normal signers, except that
158+
they sign an event instead of a transport request. The properties passed to this
159+
signing method are identical to those pased to the request signer.
160+
161+
```python
162+
class EventSigner[I, SP: Mapping[str, Any]](Protocol):
163+
164+
# TODO: add a protocol type for events
165+
async def sign(self, *, event: Any, identity: I, properties: SP) -> Any:
166+
...
167+
```
168+
169+
## Configuration
170+
171+
All services with at least one auth trait will have the following properites on
172+
their configuration object.
173+
174+
```python
175+
class AuthConfig[R: Request](Protocol):
176+
auth_scheme_resolver: AuthSchemeResolver
177+
auth_schemes: dict[ShapeID, AuthScheme[R, Any, Any, Any]]
178+
```

0 commit comments

Comments
 (0)