|
| 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