aws_smithy_runtime_api/client/
auth.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! APIs for request authentication.
7
8use crate::box_error::BoxError;
9use crate::client::identity::{Identity, SharedIdentityResolver};
10use crate::client::orchestrator::HttpRequest;
11use crate::client::runtime_components::sealed::ValidateConfig;
12use crate::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
13use crate::impl_shared_conversions;
14use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Storable, StoreReplace};
15use aws_smithy_types::type_erasure::TypeErasedBox;
16use aws_smithy_types::Document;
17use std::borrow::Cow;
18use std::fmt;
19use std::sync::Arc;
20
21/// Auth schemes for the HTTP `Authorization` header.
22#[cfg(feature = "http-auth")]
23pub mod http;
24
25/// Static auth scheme option resolver.
26pub mod static_resolver;
27
28/// The output type from the [`ResolveAuthSchemeOptions::resolve_auth_scheme_options_v2`]
29///
30/// The resolver returns a list of these, in the order the auth scheme resolver wishes to use them.
31#[derive(Clone, Debug)]
32pub struct AuthSchemeOption {
33    scheme_id: AuthSchemeId,
34    properties: Option<FrozenLayer>,
35}
36
37impl AuthSchemeOption {
38    /// Builder struct for [`AuthSchemeOption`]
39    pub fn builder() -> AuthSchemeOptionBuilder {
40        AuthSchemeOptionBuilder::default()
41    }
42
43    /// Returns [`AuthSchemeId`], the ID of the scheme
44    pub fn scheme_id(&self) -> &AuthSchemeId {
45        &self.scheme_id
46    }
47
48    /// Returns optional properties for identity resolution or signing
49    ///
50    /// This config layer is applied to the [`ConfigBag`] to ensure the information is
51    /// available during both the identity resolution and signature generation processes.
52    pub fn properties(&self) -> Option<FrozenLayer> {
53        self.properties.clone()
54    }
55}
56
57impl From<AuthSchemeId> for AuthSchemeOption {
58    fn from(auth_scheme_id: AuthSchemeId) -> Self {
59        AuthSchemeOption::builder()
60            .scheme_id(auth_scheme_id)
61            .build()
62            .expect("required fields set")
63    }
64}
65
66/// Builder struct for [`AuthSchemeOption`]
67#[derive(Debug, Default)]
68pub struct AuthSchemeOptionBuilder {
69    scheme_id: Option<AuthSchemeId>,
70    properties: Option<FrozenLayer>,
71}
72
73impl AuthSchemeOptionBuilder {
74    /// Sets [`AuthSchemeId`] for the builder
75    pub fn scheme_id(mut self, auth_scheme_id: AuthSchemeId) -> Self {
76        self.set_scheme_id(Some(auth_scheme_id));
77        self
78    }
79
80    /// Sets [`AuthSchemeId`] for the builder
81    pub fn set_scheme_id(&mut self, auth_scheme_id: Option<AuthSchemeId>) {
82        self.scheme_id = auth_scheme_id;
83    }
84
85    /// Sets the properties for the builder
86    pub fn properties(mut self, properties: FrozenLayer) -> Self {
87        self.set_properties(Some(properties));
88        self
89    }
90
91    /// Sets the properties for the builder
92    pub fn set_properties(&mut self, properties: Option<FrozenLayer>) {
93        self.properties = properties;
94    }
95
96    /// Builds an [`AuthSchemeOption`], otherwise returns an [`AuthSchemeOptionBuilderError`] in the case of error
97    pub fn build(self) -> Result<AuthSchemeOption, AuthSchemeOptionBuilderError> {
98        let scheme_id = self
99            .scheme_id
100            .ok_or(ErrorKind::MissingRequiredField("auth_scheme_id"))?;
101        Ok(AuthSchemeOption {
102            scheme_id,
103            properties: self.properties,
104        })
105    }
106}
107
108#[derive(Debug)]
109enum ErrorKind {
110    MissingRequiredField(&'static str),
111}
112
113impl From<ErrorKind> for AuthSchemeOptionBuilderError {
114    fn from(kind: ErrorKind) -> Self {
115        Self { kind }
116    }
117}
118
119/// The error type returned when failing to build [`AuthSchemeOption`] from the builder
120#[derive(Debug)]
121pub struct AuthSchemeOptionBuilderError {
122    kind: ErrorKind,
123}
124
125impl fmt::Display for AuthSchemeOptionBuilderError {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self.kind {
128            ErrorKind::MissingRequiredField(name) => {
129                write!(f, "`{name}` is required")
130            }
131        }
132    }
133}
134
135impl std::error::Error for AuthSchemeOptionBuilderError {}
136
137/// New type around an auth scheme ID.
138///
139/// Each auth scheme must have a unique string identifier associated with it,
140/// which is used to refer to auth schemes by the auth scheme option resolver, and
141/// also used to select an identity resolver to use.
142#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
143pub struct AuthSchemeId {
144    scheme_id: Cow<'static, str>,
145}
146
147// See: https://doc.rust-lang.org/std/convert/trait.AsRef.html#reflexivity
148impl AsRef<AuthSchemeId> for AuthSchemeId {
149    fn as_ref(&self) -> &AuthSchemeId {
150        self
151    }
152}
153
154// Normalizes auth scheme IDs for comparison and hashing by treating "no_auth" and "noAuth" as equivalent
155// by converting "no_auth" to "noAuth".
156// This is for backward compatibility; "no_auth" was incorrectly used in earlier GA versions of the SDK and
157// could be used still in some places.
158const fn normalize_auth_scheme_id(s: &'static str) -> &'static str {
159    match s.as_bytes() {
160        b"no_auth" => "noAuth",
161        _ => s,
162    }
163}
164
165impl AuthSchemeId {
166    /// Creates a new auth scheme ID.
167    pub const fn new(scheme_id: &'static str) -> Self {
168        Self {
169            scheme_id: Cow::Borrowed(normalize_auth_scheme_id(scheme_id)),
170        }
171    }
172
173    /// Returns the string equivalent of this auth scheme ID.
174    #[deprecated(
175        note = "This function is no longer functional. Use `inner` instead",
176        since = "1.8.0"
177    )]
178    pub const fn as_str(&self) -> &'static str {
179        match self.scheme_id {
180            Cow::Borrowed(val) => val,
181            Cow::Owned(_) => {
182                // cannot obtain `&'static str` from `String` unless we use `Box::leak`
183                ""
184            }
185        }
186    }
187
188    /// Returns the string equivalent of this auth scheme ID.
189    pub fn inner(&self) -> &str {
190        &self.scheme_id
191    }
192}
193
194impl From<&'static str> for AuthSchemeId {
195    fn from(scheme_id: &'static str) -> Self {
196        Self::new(scheme_id)
197    }
198}
199
200impl From<Cow<'static, str>> for AuthSchemeId {
201    fn from(scheme_id: Cow<'static, str>) -> Self {
202        let normalized_scheme_id = match &scheme_id {
203            Cow::Borrowed(s) => Cow::Borrowed(normalize_auth_scheme_id(s)),
204            Cow::Owned(s) => {
205                if s == "no_auth" {
206                    Cow::Borrowed("noAuth")
207                } else {
208                    scheme_id
209                }
210            }
211        };
212        Self {
213            scheme_id: normalized_scheme_id,
214        }
215    }
216}
217
218/// Parameters needed to resolve auth scheme options.
219///
220/// Most generated clients will use the [`StaticAuthSchemeOptionResolver`](static_resolver::StaticAuthSchemeOptionResolver),
221/// which doesn't require any parameters for resolution (and has its own empty params struct).
222///
223/// However, more complex auth scheme resolvers may need modeled parameters in order to resolve
224/// the auth scheme options. For those, this params struct holds a type erased box so that any
225/// kind of parameters can be contained within, and type casted by the auth scheme option resolver
226/// implementation.
227#[derive(Debug)]
228pub struct AuthSchemeOptionResolverParams(TypeErasedBox);
229
230impl AuthSchemeOptionResolverParams {
231    /// Creates a new [`AuthSchemeOptionResolverParams`].
232    pub fn new<T: fmt::Debug + Send + Sync + 'static>(params: T) -> Self {
233        Self(TypeErasedBox::new(params))
234    }
235
236    /// Returns the underlying parameters as the type `T` if they are that type.
237    pub fn get<T: fmt::Debug + Send + Sync + 'static>(&self) -> Option<&T> {
238        self.0.downcast_ref()
239    }
240}
241
242impl Storable for AuthSchemeOptionResolverParams {
243    type Storer = StoreReplace<Self>;
244}
245
246new_type_future! {
247    #[doc = "Future for [`ResolveAuthSchemeOptions::resolve_auth_scheme_options_v2`]."]
248    pub struct AuthSchemeOptionsFuture<'a, Vec<AuthSchemeOption>, BoxError>;
249}
250
251/// Resolver for auth scheme options.
252///
253/// The orchestrator needs to select an auth scheme to sign requests with, and potentially
254/// from several different available auth schemes. Smithy models have a number of ways
255/// to specify which operations can use which auth schemes under which conditions, as
256/// documented in the [Smithy spec](https://smithy.io/2.0/spec/authentication-traits.html).
257///
258/// The orchestrator uses the auth scheme option resolver runtime component to resolve
259/// an ordered list of options that are available to choose from for a given request.
260/// This resolver can be a simple static list, such as with the
261/// [`StaticAuthSchemeOptionResolver`](static_resolver::StaticAuthSchemeOptionResolver),
262/// or it can be a complex code generated resolver that incorporates parameters from both
263/// the model and the resolved endpoint.
264pub trait ResolveAuthSchemeOptions: Send + Sync + fmt::Debug {
265    #[deprecated(
266        note = "This method is deprecated, use `resolve_auth_scheme_options_v2` instead.",
267        since = "1.8.0"
268    )]
269    /// Returns a list of available auth scheme options to choose from.
270    fn resolve_auth_scheme_options(
271        &self,
272        _params: &AuthSchemeOptionResolverParams,
273    ) -> Result<Cow<'_, [AuthSchemeId]>, BoxError> {
274        unimplemented!("This method is deprecated, use `resolve_auth_scheme_options_v2` instead.");
275    }
276
277    #[allow(deprecated)]
278    /// Returns a list of available auth scheme options to choose from.
279    fn resolve_auth_scheme_options_v2<'a>(
280        &'a self,
281        params: &'a AuthSchemeOptionResolverParams,
282        _cfg: &'a ConfigBag,
283        _runtime_components: &'a RuntimeComponents,
284    ) -> AuthSchemeOptionsFuture<'a> {
285        AuthSchemeOptionsFuture::ready({
286            self.resolve_auth_scheme_options(params).map(|options| {
287                options
288                    .iter()
289                    .cloned()
290                    .map(|scheme_id| {
291                        AuthSchemeOption::builder()
292                            .scheme_id(scheme_id)
293                            .build()
294                            .expect("required fields set")
295                    })
296                    .collect::<Vec<_>>()
297            })
298        })
299    }
300}
301
302/// A shared auth scheme option resolver.
303#[derive(Clone, Debug)]
304pub struct SharedAuthSchemeOptionResolver(Arc<dyn ResolveAuthSchemeOptions>);
305
306impl SharedAuthSchemeOptionResolver {
307    /// Creates a new [`SharedAuthSchemeOptionResolver`].
308    pub fn new(auth_scheme_option_resolver: impl ResolveAuthSchemeOptions + 'static) -> Self {
309        Self(Arc::new(auth_scheme_option_resolver))
310    }
311}
312
313impl ResolveAuthSchemeOptions for SharedAuthSchemeOptionResolver {
314    #[allow(deprecated)]
315    fn resolve_auth_scheme_options(
316        &self,
317        params: &AuthSchemeOptionResolverParams,
318    ) -> Result<Cow<'_, [AuthSchemeId]>, BoxError> {
319        (*self.0).resolve_auth_scheme_options(params)
320    }
321
322    fn resolve_auth_scheme_options_v2<'a>(
323        &'a self,
324        params: &'a AuthSchemeOptionResolverParams,
325        cfg: &'a ConfigBag,
326        runtime_components: &'a RuntimeComponents,
327    ) -> AuthSchemeOptionsFuture<'a> {
328        (*self.0).resolve_auth_scheme_options_v2(params, cfg, runtime_components)
329    }
330}
331
332impl_shared_conversions!(
333    convert SharedAuthSchemeOptionResolver
334    from ResolveAuthSchemeOptions
335    using SharedAuthSchemeOptionResolver::new
336);
337
338/// An auth scheme.
339///
340/// Auth schemes have unique identifiers (the `scheme_id`),
341/// and provide an identity resolver and a signer.
342pub trait AuthScheme: Send + Sync + fmt::Debug {
343    /// Returns the unique identifier associated with this auth scheme.
344    ///
345    /// This identifier is used to refer to this auth scheme from the
346    /// [`ResolveAuthSchemeOptions`], and is also associated with
347    /// identity resolvers in the config.
348    fn scheme_id(&self) -> AuthSchemeId;
349
350    /// Returns the identity resolver that can resolve an identity for this scheme, if one is available.
351    ///
352    /// The [`AuthScheme`] doesn't actually own an identity resolver. Rather, identity resolvers
353    /// are configured as runtime components. The auth scheme merely chooses a compatible identity
354    /// resolver from the runtime components via the [`GetIdentityResolver`] trait. The trait is
355    /// given rather than the full set of runtime components to prevent complex resolution logic
356    /// involving multiple components from taking place in this function, since that's not the
357    /// intended use of this design.
358    fn identity_resolver(
359        &self,
360        identity_resolvers: &dyn GetIdentityResolver,
361    ) -> Option<SharedIdentityResolver>;
362
363    /// Returns the signing implementation for this auth scheme.
364    fn signer(&self) -> &dyn Sign;
365}
366
367/// Container for a shared auth scheme implementation.
368#[derive(Clone, Debug)]
369pub struct SharedAuthScheme(Arc<dyn AuthScheme>);
370
371impl SharedAuthScheme {
372    /// Creates a new [`SharedAuthScheme`] from the given auth scheme.
373    pub fn new(auth_scheme: impl AuthScheme + 'static) -> Self {
374        Self(Arc::new(auth_scheme))
375    }
376}
377
378impl AuthScheme for SharedAuthScheme {
379    fn scheme_id(&self) -> AuthSchemeId {
380        self.0.scheme_id()
381    }
382
383    fn identity_resolver(
384        &self,
385        identity_resolvers: &dyn GetIdentityResolver,
386    ) -> Option<SharedIdentityResolver> {
387        self.0.identity_resolver(identity_resolvers)
388    }
389
390    fn signer(&self) -> &dyn Sign {
391        self.0.signer()
392    }
393}
394
395impl ValidateConfig for SharedAuthScheme {}
396
397impl_shared_conversions!(convert SharedAuthScheme from AuthScheme using SharedAuthScheme::new);
398
399/// Signing implementation for an auth scheme.
400pub trait Sign: Send + Sync + fmt::Debug {
401    /// Sign the given request with the given identity, components, and config.
402    ///
403    /// If the provided identity is incompatible with this signer, an error must be returned.
404    fn sign_http_request(
405        &self,
406        request: &mut HttpRequest,
407        identity: &Identity,
408        auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
409        runtime_components: &RuntimeComponents,
410        config_bag: &ConfigBag,
411    ) -> Result<(), BoxError>;
412}
413
414/// Endpoint configuration for the selected auth scheme.
415///
416/// The configuration held by this struct originates from the endpoint rule set in the service model.
417///
418/// This struct gets added to the request state by the auth orchestrator.
419#[non_exhaustive]
420#[derive(Clone, Debug)]
421pub struct AuthSchemeEndpointConfig<'a>(Option<&'a Document>);
422
423impl<'a> AuthSchemeEndpointConfig<'a> {
424    /// Creates an empty [`AuthSchemeEndpointConfig`].
425    pub fn empty() -> Self {
426        Self(None)
427    }
428
429    /// Returns the endpoint configuration as a [`Document`].
430    pub fn as_document(&self) -> Option<&'a Document> {
431        self.0
432    }
433}
434
435impl<'a> From<Option<&'a Document>> for AuthSchemeEndpointConfig<'a> {
436    fn from(value: Option<&'a Document>) -> Self {
437        Self(value)
438    }
439}
440
441impl<'a> From<&'a Document> for AuthSchemeEndpointConfig<'a> {
442    fn from(value: &'a Document) -> Self {
443        Self(Some(value))
444    }
445}
446
447/// An ordered list of [AuthSchemeId]s
448///
449/// Can be used to reorder already-resolved auth schemes by an auth scheme resolver.
450/// This list is intended as a hint rather than a strict override;
451/// any schemes not present in the resolved auth schemes will be ignored.
452#[derive(Clone, Debug, Default, Eq, PartialEq)]
453pub struct AuthSchemePreference {
454    preference_list: Vec<AuthSchemeId>,
455}
456
457impl Storable for AuthSchemePreference {
458    type Storer = StoreReplace<Self>;
459}
460
461impl IntoIterator for AuthSchemePreference {
462    type Item = AuthSchemeId;
463    type IntoIter = std::vec::IntoIter<Self::Item>;
464
465    fn into_iter(self) -> Self::IntoIter {
466        self.preference_list.into_iter()
467    }
468}
469
470impl<T> From<T> for AuthSchemePreference
471where
472    T: AsRef<[AuthSchemeId]>,
473{
474    fn from(slice: T) -> Self {
475        AuthSchemePreference {
476            preference_list: slice.as_ref().to_vec(),
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn test_auth_scheme_id_new_normalizes_no_auth() {
487        // Test that "no_auth" gets normalized to "noAuth"
488        let auth_scheme_id = AuthSchemeId::new("no_auth");
489        assert_eq!(auth_scheme_id.inner(), "noAuth");
490    }
491
492    #[test]
493    fn test_auth_scheme_id_new_preserves_no_auth_camel_case() {
494        // Test that "noAuth" remains unchanged
495        let auth_scheme_id = AuthSchemeId::new("noAuth");
496        assert_eq!(auth_scheme_id.inner(), "noAuth");
497    }
498
499    #[test]
500    fn test_auth_scheme_id_new_preserves_other_schemes() {
501        // Test that other auth scheme IDs are not modified
502        let test_cases = [
503            "sigv4",
504            "sigv4a",
505            "httpBearerAuth",
506            "httpBasicAuth",
507            "custom_auth",
508            "bearer",
509            "basic",
510        ];
511
512        for scheme in test_cases {
513            let auth_scheme_id = AuthSchemeId::new(scheme);
514            assert_eq!(auth_scheme_id.inner(), scheme);
515        }
516    }
517
518    #[test]
519    fn test_auth_scheme_id_equality_after_normalization() {
520        // Test that "no_auth" and "noAuth" are considered equal after normalization
521        let no_auth_underscore = AuthSchemeId::new("no_auth");
522        let no_auth_camel = AuthSchemeId::new("noAuth");
523
524        assert_eq!(no_auth_underscore, no_auth_camel);
525        assert_eq!(no_auth_underscore.inner(), no_auth_camel.inner());
526    }
527
528    #[test]
529    fn test_auth_scheme_id_hash_consistency_after_normalization() {
530        use std::collections::HashMap;
531
532        // Test that normalized IDs have consistent hashing behavior
533        let mut map = HashMap::new();
534        let no_auth_underscore = AuthSchemeId::new("no_auth");
535        let no_auth_camel = AuthSchemeId::new("noAuth");
536
537        map.insert(no_auth_underscore.clone(), "value1");
538        map.insert(no_auth_camel.clone(), "value2");
539
540        // Should only have one entry since they normalize to the same value
541        assert_eq!(map.len(), 1);
542        assert_eq!(map.get(&no_auth_underscore), Some(&"value2"));
543        assert_eq!(map.get(&no_auth_camel), Some(&"value2"));
544    }
545
546    #[test]
547    fn test_auth_scheme_id_ordering_after_normalization() {
548        // Test that ordering works correctly with normalized values
549        let no_auth_underscore = AuthSchemeId::new("no_auth");
550        let no_auth_camel = AuthSchemeId::new("noAuth");
551        let other_scheme = AuthSchemeId::new("sigv4");
552
553        assert_eq!(
554            no_auth_underscore.cmp(&no_auth_camel),
555            std::cmp::Ordering::Equal
556        );
557        assert_eq!(no_auth_underscore.cmp(&other_scheme), "noAuth".cmp("sigv4"));
558    }
559
560    #[test]
561    fn test_normalize_auth_scheme_id_function() {
562        // Test the normalize function directly
563        assert_eq!(normalize_auth_scheme_id("no_auth"), "noAuth");
564        assert_eq!(normalize_auth_scheme_id("noAuth"), "noAuth");
565        assert_eq!(normalize_auth_scheme_id("sigv4"), "sigv4");
566        assert_eq!(normalize_auth_scheme_id("custom"), "custom");
567    }
568
569    #[test]
570    fn test_auth_scheme_id_from_cow_borrowed_normalizes_no_auth() {
571        // Test that Cow::Borrowed("no_auth") gets normalized to "noAuth"
572        let auth_scheme_id = AuthSchemeId::from(Cow::Borrowed("no_auth"));
573        assert_eq!(auth_scheme_id.inner(), "noAuth");
574    }
575
576    #[test]
577    fn test_auth_scheme_id_from_cow_borrowed_preserves_no_auth_camel_case() {
578        // Test that Cow::Borrowed("noAuth") remains unchanged
579        let auth_scheme_id = AuthSchemeId::from(Cow::Borrowed("noAuth"));
580        assert_eq!(auth_scheme_id.inner(), "noAuth");
581    }
582
583    #[test]
584    fn test_auth_scheme_id_from_cow_owned_normalizes_no_auth() {
585        // Test that Cow::Owned(String::from("no_auth")) gets normalized to "noAuth"
586        let auth_scheme_id = AuthSchemeId::from(Cow::Owned(String::from("no_auth")));
587        assert_eq!(auth_scheme_id.inner(), "noAuth");
588    }
589
590    #[test]
591    fn test_auth_scheme_id_from_cow_owned_preserves_no_auth_camel_case() {
592        // Test that Cow::Owned(String::from("noAuth")) remains unchanged
593        let auth_scheme_id = AuthSchemeId::from(Cow::Owned(String::from("noAuth")));
594        assert_eq!(auth_scheme_id.inner(), "noAuth");
595    }
596
597    #[test]
598    fn test_auth_scheme_id_from_cow_between_borrowed_and_owned_mixing_updated_and_legacy() {
599        let borrowed_no_auth = AuthSchemeId::from(Cow::Borrowed("noAuth"));
600        let owned_no_auth = AuthSchemeId::from(Cow::Owned(String::from("no_auth")));
601
602        assert_eq!(borrowed_no_auth, owned_no_auth);
603        assert_eq!(borrowed_no_auth.inner(), owned_no_auth.inner());
604    }
605}