Skip to content

Identity and Authentication

Smithy services may define any number of authentication schemes via traits and configure which schemes are available and prioritized on a per-operation basis. This document describes how an auth scheme is configured and picked at runtime.

Auth Schemes

Everything to do with an auth scheme is contained within an implementation of the AuthScheme Protocol. These implementations construct the identity resolvers and signers as well as the extra properties needed for identity resolution and signing.

Each AuthScheme has a scheme_id, which is the Smithy shape ID of the auth scheme.

class AuthScheme[R: Request, I: Identity, IP: Mapping[str, Any], SP: Mapping[str, Any]](
    Protocol
):
    scheme_id: ShapeID

    def identity_properties(self, *, context: _TypedProperties) -> IP:
        ...

    def identity_resolver(
        self, *, context: _TypedProperties
    ) -> IdentityResolver[I, IP]:
        ...

    def signer_properties(self, *, context: _TypedProperties) -> SP:
        ...

    def signer(self) -> Signer[R, I, SP]:
        ...

    def event_signer(self, *, request: R) -> EventSigner[I, SP] | None:
        return None

AuthScheme implementations SHOULD cache identity resolvers and signers if possible.

Auth Scheme Resolution

Services and operation may support any number of auth schemes, each of which may or may not be availble for a number of reasons, such as not being configured. An AuthSchemeResolver is used to figure out which auth scheme to use for each request.

class AuthSchemeResolver(Protocol):
    def resolve_auth_scheme(
        self, *, auth_parameters: AuthParams[Any, Any]
    ) -> Sequence[AuthOption]:
        ...

class AuthOption(Protocol):
    scheme_id: ShapeID
    identity_properties: TypedProperties
    signer_properties: TypedProperties

@dataclass(kw_only=True, frozen=True)
class AuthParams[I: SerializeableShape, O: DeserializeableShape]:
    protocol_id: ShapeID
    operation: APIOperation[I, O]
    context: TypedProperties

The resolver is given the ID of the protocol being used by the client, the schema of the operation being invoked, and the operation invocation context. It returns a priority-ordered list of auth schemes to pick from, along with optional overrides for identity and signer properties.

The client will pick the first auth scheme in the list that has an entry in the auth_schemes configuration dict and which is able to resolve an identity.

The resolver itself is stored in the service's configuration object, and may be replaced with a custom implemenatation. Default implementations are generated based on the modeled auth traits.

Identity

Each auth scheme is associated with an identity type, such as an API key or username and password. In the AWS context, this is the access key id, secret access key, and optionally the session token.

Identities MAY be shared between multiple auth schemes. For example, the AWS sigv4 and sigv4a auth schemes use the same AWS identity.

In Python, each identity type MUST implement the following Protocol:

@runtime_checkable
class Identity(Protocol):

    expiration: datetime | None = None

    @property
    def is_expired(self) -> bool:
        if self.expiration is None:
            return False
        return datetime.now(tz=UTC) >= self.expiration

An Identity may be derived from any number of sources, such as configuration properties or environement variables. These different sources are loaded by an IdentityResolver.

Identity Resolvers

Identity resolvers are responsible for contructiong an Identity for a request.

class IdentityResolver[I: Identity, IP: Mapping[str, Any]](Protocol):

    async def get_identity(self, *, properties: IP) -> I:
        ...

Each identity source SHOULD have its own identity resolver implementation. If an Identity is supported by multiple IdentityResolvers, those resolver SHOULD be prioritized to provide a stable resolution strategy. A ChainedIdentityResolver implementation is provided that implements this behavior generically.

The get_identity function takes only one (keyword-only) argument - a mapping of properties that is refined by the IP generic parameter. The identity properties are contructed by the AuthScheme's identity_properties method.

Identity resolvers are constructed by the AuthScheme's identity_resolver method.

Signers

Signers are responsible for signing transport requests so that they can be authenticated by the server. They are given the transport request to sign, the resolved identity, and a property mapping that is used for any additional configuration needed. The signing properties are constructed by the AuthScheme's signer_properties method.

class Signer[R: Request, I, SP: Mapping[str, Any]](Protocol):
    async def sign(self, *, request: R, identity: I, properties: SP) -> R:
        ...

Signers are constructed by the AuthScheme's signer method.

Signers MAY modify the given request and return it, or construct a new signed request.

Event Signers

Auh schemes MAY also have an associated event signer, which signs events that are sent to a server. They behave in the same way as normal signers, except that they sign an event instead of a transport request. The properties passed to this signing method are identical to those pased to the request signer.

class EventSigner[I, SP: Mapping[str, Any]](Protocol):

    # TODO: add a protocol type for events
    async def sign(self, *, event: Any, identity: I, properties: SP) -> Any:
        ...

Configuration

All services with at least one auth trait will have the following properites on their configuration object.

class AuthConfig[R: Request](Protocol):
    auth_scheme_resolver: AuthSchemeResolver
    auth_schemes: dict[ShapeID, AuthScheme[R, Any, Any, Any]]