aws_smithy_runtime/client/orchestrator/
auth.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::client::auth::no_auth::NO_AUTH_SCHEME_ID;
7use crate::client::identity::IdentityCache;
8use aws_smithy_runtime_api::box_error::BoxError;
9use aws_smithy_runtime_api::client::auth::{
10    AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOption,
11    AuthSchemeOptionResolverParams, AuthSchemePreference, ResolveAuthSchemeOptions,
12};
13use aws_smithy_runtime_api::client::endpoint::{EndpointResolverParams, ResolveEndpoint};
14use aws_smithy_runtime_api::client::identity::{Identity, ResolveIdentity};
15use aws_smithy_runtime_api::client::identity::{IdentityCacheLocation, ResolveCachedIdentity};
16use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
17use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
18use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Storable, StoreReplace};
19use aws_smithy_types::endpoint::Endpoint;
20use aws_smithy_types::Document;
21use std::borrow::Cow;
22use std::collections::HashMap;
23use std::error::Error as StdError;
24use std::fmt;
25use tracing::trace;
26
27#[derive(Debug)]
28struct NoMatchingAuthSchemeError(ExploredList);
29
30impl fmt::Display for NoMatchingAuthSchemeError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        let explored = &self.0;
33
34        // Use the information we have about the auth options that were explored to construct
35        // as helpful of an error message as possible.
36        if explored.items().count() == 0 {
37            return f.write_str(
38                "no auth options are available. This can happen if there's \
39                    a problem with the service model, or if there is a codegen bug.",
40            );
41        }
42        if explored
43            .items()
44            .all(|explored| matches!(explored.result, ExploreResult::NoAuthScheme))
45        {
46            return f.write_str(
47                "no auth schemes are registered. This can happen if there's \
48                    a problem with the service model, or if there is a codegen bug.",
49            );
50        }
51
52        let mut try_add_identity = false;
53        let mut likely_bug = false;
54        f.write_str("failed to select an auth scheme to sign the request with.")?;
55        for item in explored.items() {
56            write!(
57                f,
58                " \"{}\" wasn't a valid option because ",
59                item.scheme_id.inner()
60            )?;
61            f.write_str(match item.result {
62                ExploreResult::NoAuthScheme => {
63                    likely_bug = true;
64                    "no auth scheme was registered for it."
65                }
66                ExploreResult::NoIdentityResolver => {
67                    try_add_identity = true;
68                    "there was no identity resolver for it."
69                }
70                ExploreResult::MissingEndpointConfig => {
71                    likely_bug = true;
72                    "there is auth config in the endpoint config, but this scheme wasn't listed in it \
73                    (see https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details)."
74                }
75                ExploreResult::NotExplored => {
76                    debug_assert!(false, "this should be unreachable");
77                    "<unknown>"
78                }
79            })?;
80        }
81        if try_add_identity {
82            f.write_str(" Be sure to set an identity, such as credentials, auth token, or other identity type that is required for this service.")?;
83        } else if likely_bug {
84            f.write_str(" This is likely a bug.")?;
85        }
86        if explored.truncated {
87            f.write_str(" Note: there were other auth schemes that were evaluated that weren't listed here.")?;
88        }
89
90        Ok(())
91    }
92}
93
94impl StdError for NoMatchingAuthSchemeError {}
95
96#[derive(Debug)]
97enum AuthOrchestrationError {
98    MissingEndpointConfig,
99    BadAuthSchemeEndpointConfig(Cow<'static, str>),
100    FailedToResolveEndpoint(BoxError),
101}
102
103impl fmt::Display for AuthOrchestrationError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            // This error is never bubbled up
107            Self::MissingEndpointConfig => f.write_str("missing endpoint config"),
108            Self::BadAuthSchemeEndpointConfig(message) => f.write_str(message),
109            Self::FailedToResolveEndpoint(source) => {
110                write!(f, "failed to resolve endpoint: {source}")
111            }
112        }
113    }
114}
115
116impl StdError for AuthOrchestrationError {}
117
118pub(super) async fn resolve_identity(
119    runtime_components: &RuntimeComponents,
120    cfg: &mut ConfigBag,
121) -> Result<(AuthSchemeId, Identity, Option<Endpoint>), BoxError> {
122    let params = cfg
123        .load::<AuthSchemeOptionResolverParams>()
124        .expect("auth scheme option resolver params must be set");
125    let option_resolver = runtime_components.auth_scheme_option_resolver();
126    let options = option_resolver
127        .resolve_auth_scheme_options_v2(params, cfg, runtime_components)
128        .await?;
129    let options =
130        reprioritize_with_auth_scheme_preference(options, cfg.load::<AuthSchemePreference>()).await;
131
132    trace!(
133        auth_scheme_option_resolver_params = ?params,
134        auth_scheme_options = ?options,
135        "orchestrating auth",
136    );
137
138    let mut explored = ExploredList::default();
139
140    // Iterate over IDs of possibly-supported auth schemes
141    for auth_scheme_option in &options {
142        let scheme_id = auth_scheme_option.scheme_id();
143        // For each ID, try to resolve the corresponding auth scheme.
144        if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) {
145            // Use the resolved auth scheme to resolve an identity
146            if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) {
147                match legacy_try_resolve_endpoint(runtime_components, cfg, scheme_id).await {
148                    Ok(endpoint) => {
149                        trace!(scheme_id= ?scheme_id, "resolving identity");
150                        let identity_cache = if identity_resolver.cache_location()
151                            == IdentityCacheLocation::RuntimeComponents
152                        {
153                            runtime_components.identity_cache()
154                        } else {
155                            IdentityCache::no_cache()
156                        };
157                        // Apply properties from the selected auth scheme option
158                        if let Some(properties) = auth_scheme_option.properties() {
159                            cfg.push_shared_layer(properties);
160                        }
161                        let identity = identity_cache
162                            .resolve_cached_identity(identity_resolver, runtime_components, cfg)
163                            .await?;
164                        trace!(identity = ?identity, "resolved identity");
165                        // Extract the FrozenLayer placed in the Identity property bag by the From<Credentials> impl.
166                        // This layer contains feature data for the user agent and potentially other metadata.
167                        if let Some(layer) = identity.property::<FrozenLayer>().cloned() {
168                            cfg.push_shared_layer(layer);
169                        }
170                        return Ok((scheme_id.clone(), identity, endpoint));
171                    }
172                    Err(AuthOrchestrationError::MissingEndpointConfig) => {
173                        explored.push(scheme_id.clone(), ExploreResult::MissingEndpointConfig);
174                        continue;
175                    }
176                    Err(AuthOrchestrationError::FailedToResolveEndpoint(source)) => {
177                        // Some negative endpoint tests expect an endpoint resolution error,
178                        // so we need to return it to satisfy them.
179                        return Err(source);
180                    }
181                    Err(other_err) => {
182                        return Err(other_err.into());
183                    }
184                }
185            } else {
186                explored.push(scheme_id.clone(), ExploreResult::NoIdentityResolver);
187            }
188        } else {
189            explored.push(scheme_id.clone(), ExploreResult::NoAuthScheme);
190        }
191    }
192
193    Err(NoMatchingAuthSchemeError(explored).into())
194}
195
196// Re-prioritize `supported_auth_scheme_options` based on `auth_scheme_preference`
197//
198// Schemes in `auth_scheme_preference` that are not present in `supported_auth_scheme_options` will be ignored.
199async fn reprioritize_with_auth_scheme_preference(
200    supported_auth_scheme_options: Vec<AuthSchemeOption>,
201    auth_scheme_preference: Option<&AuthSchemePreference>,
202) -> Vec<AuthSchemeOption> {
203    match auth_scheme_preference {
204        Some(preference) => {
205            // maps auth scheme ID to the index in the preference list
206            let preference_map: HashMap<_, _> = preference
207                .clone()
208                .into_iter()
209                .enumerate()
210                .map(|(i, s)| (s, i))
211                .collect();
212            let (mut preferred, non_preferred): (Vec<_>, Vec<_>) = supported_auth_scheme_options
213                .into_iter()
214                .partition(|auth_scheme_option| {
215                    preference_map.contains_key(auth_scheme_option.scheme_id())
216                });
217
218            preferred.sort_by_key(|opt| {
219                preference_map
220                    .get(opt.scheme_id())
221                    .expect("guaranteed by `partition`")
222            });
223            preferred.extend(non_preferred);
224            preferred
225        }
226        None => supported_auth_scheme_options,
227    }
228}
229
230pub(super) fn sign_request(
231    scheme_id: &AuthSchemeId,
232    identity: &Identity,
233    ctx: &mut InterceptorContext,
234    runtime_components: &RuntimeComponents,
235    cfg: &ConfigBag,
236) -> Result<(), BoxError> {
237    trace!("signing request");
238    let request = ctx.request_mut().expect("set during serialization");
239    let endpoint = cfg
240        .load::<Endpoint>()
241        .expect("endpoint added to config bag by endpoint orchestrator");
242    let auth_scheme = runtime_components
243        .auth_scheme(scheme_id)
244        .ok_or("should be configured")?;
245    let signer = auth_scheme.signer();
246    let auth_scheme_endpoint_config = extract_endpoint_auth_scheme_config(endpoint, scheme_id)?;
247    trace!(
248        signer = ?signer,
249        "signing implementation"
250    );
251    signer.sign_http_request(
252        request,
253        identity,
254        auth_scheme_endpoint_config,
255        runtime_components,
256        cfg,
257    )?;
258    Ok(())
259}
260
261// Marker indicating the correct resolution order: auth scheme resolution, identity resolution,
262// and then endpoint resolution, as specified in the SRA.
263//
264// This marker is included in the config bag to signify the intended resolution order
265// by design. When the crate was released for GA, the resolution order was reversed
266// (endpoint resolution first, followed by identity resolution). However, we later
267// discovered the order needed correcting without breaking existing SDKs.
268// This marker signals the runtime to support both resolution orders without introducing
269// `aws-smithy-runtime` version 2.x.
270//
271// When this marker is present in the config bag (the default behavior for forward compatibility),
272// `resolve_identity` skips endpoint resolution, ensuring that `try_attempt` follows
273// the correct resolution order: auth scheme → identity → endpoint.
274// If the marker is absent, `try_attempt` continues using
275// the legacy, incorrect resolution order: endpoint → auth scheme → identity.
276#[doc(hidden)]
277#[derive(Clone, Debug)]
278pub struct AuthSchemeAndEndpointOrchestrationV2;
279
280impl Storable for AuthSchemeAndEndpointOrchestrationV2 {
281    type Storer = StoreReplace<Self>;
282}
283
284// Conditionally return an `endpoint` resolved by `SharedEndpointResolver` in `runtime_components`
285// whose `authSchemes` property matches the given `scheme_id`
286//
287// Return the `Ok` variant in the following cases:
288// - If `AuthSchemeAndEndpointOrchestrationV2` is present in the config bag, indicating that the runtime doesn't require the functionality of this function.
289//   In this case, the function short-circuits and returns `Ok(None)`, as no endpoint needs resolution. Instead, a custom `AuthSchemeOptionResolver`
290//   should have resolved it internally for auth scheme selection.
291// - If the runtime uses the legacy auth scheme and endpoint orchestration and resolves an endpoint that matches the given condition,
292//   the function returns `Ok(endpoint)`, which will then be applied to the request.
293async fn legacy_try_resolve_endpoint(
294    runtime_components: &RuntimeComponents,
295    cfg: &ConfigBag,
296    scheme_id: &AuthSchemeId,
297) -> Result<Option<Endpoint>, AuthOrchestrationError> {
298    if cfg.load::<AuthSchemeAndEndpointOrchestrationV2>().is_some() {
299        // The orchestrator uses the correct auth scheme and endpoint resolution order,
300        // and no endpoint needs to be resolved within this function.
301        return Ok(None);
302    }
303
304    let params = cfg
305        .load::<EndpointResolverParams>()
306        .expect("endpoint resolver params must be set");
307
308    tracing::debug!(scheme_id = ?scheme_id, endpoint_params = ?params, "using legacy auth and endpoint orchestration, resolving endpoint for auth scheme selection");
309
310    let endpoint = runtime_components
311        .endpoint_resolver()
312        .resolve_endpoint(params)
313        .await
314        .map_err(AuthOrchestrationError::FailedToResolveEndpoint)?;
315
316    // This line repurposes `extract_endpoint_auth_scheme_config` to check whether
317    // the function returns `Ok` for the given `endpoint`.
318    // Essentially, we verify if the `authSchemes` property of `endpoint` contains `scheme_id`,
319    // which is done by `schemes.iter().find(...).ok_or(...)` within the function.
320    // However, this execution path is only exercised for legacy auth scheme and endpoint orchestration,
321    // so we don't bother refactoring the predicate out of the function.
322    let _ = extract_endpoint_auth_scheme_config(&endpoint, scheme_id)?;
323
324    Ok(Some(endpoint))
325}
326
327fn extract_endpoint_auth_scheme_config<'a>(
328    endpoint: &'a Endpoint,
329    scheme_id: &AuthSchemeId,
330) -> Result<AuthSchemeEndpointConfig<'a>, AuthOrchestrationError> {
331    // TODO(P96049742): Endpoint config doesn't currently have a concept of optional auth or "no auth", so
332    // we are short-circuiting lookup of endpoint auth scheme config if that is the selected scheme.
333    if scheme_id == &NO_AUTH_SCHEME_ID {
334        return Ok(AuthSchemeEndpointConfig::empty());
335    }
336    let auth_schemes = match endpoint.properties().get("authSchemes") {
337        Some(Document::Array(schemes)) => schemes,
338        // no auth schemes:
339        None => return Ok(AuthSchemeEndpointConfig::empty()),
340        _other => {
341            return Err(AuthOrchestrationError::BadAuthSchemeEndpointConfig(
342                "expected an array for `authSchemes` in endpoint config".into(),
343            ))
344        }
345    };
346    let auth_scheme_config = auth_schemes
347        .iter()
348        .find(|doc| {
349            let config_scheme_id = doc
350                .as_object()
351                .and_then(|object| object.get("name"))
352                .and_then(Document::as_string);
353            config_scheme_id == Some(scheme_id.inner())
354        })
355        .ok_or(AuthOrchestrationError::MissingEndpointConfig)?;
356    Ok(AuthSchemeEndpointConfig::from(Some(auth_scheme_config)))
357}
358
359#[derive(Debug)]
360enum ExploreResult {
361    NotExplored,
362    NoAuthScheme,
363    NoIdentityResolver,
364    MissingEndpointConfig,
365}
366
367/// Information about an evaluated auth option.
368/// This should be kept small so it can fit in an array on the stack.
369#[derive(Debug)]
370struct ExploredAuthOption {
371    scheme_id: AuthSchemeId,
372    result: ExploreResult,
373}
374impl Default for ExploredAuthOption {
375    fn default() -> Self {
376        Self {
377            scheme_id: AuthSchemeId::new(""),
378            result: ExploreResult::NotExplored,
379        }
380    }
381}
382
383const MAX_EXPLORED_LIST_LEN: usize = 8;
384
385/// Stack allocated list of explored auth options for error messaging
386#[derive(Default)]
387struct ExploredList {
388    items: [ExploredAuthOption; MAX_EXPLORED_LIST_LEN],
389    len: usize,
390    truncated: bool,
391}
392impl ExploredList {
393    fn items(&self) -> impl Iterator<Item = &ExploredAuthOption> {
394        self.items.iter().take(self.len)
395    }
396
397    fn push(&mut self, scheme_id: AuthSchemeId, result: ExploreResult) {
398        if self.len + 1 >= self.items.len() {
399            self.truncated = true;
400        } else {
401            self.items[self.len] = ExploredAuthOption { scheme_id, result };
402            self.len += 1;
403        }
404    }
405}
406impl fmt::Debug for ExploredList {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        f.debug_struct("ExploredList")
409            .field("items", &&self.items[0..self.len])
410            .field("truncated", &self.truncated)
411            .finish()
412    }
413}
414
415#[cfg(all(test, any(feature = "test-util", feature = "legacy-test-util")))]
416mod tests {
417    use super::*;
418    use crate::client::orchestrator::endpoints::{
419        StaticUriEndpointResolver, StaticUriEndpointResolverParams,
420    };
421    use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
422    use aws_smithy_runtime_api::client::auth::{
423        AuthScheme, AuthSchemeId, AuthSchemeOptionResolverParams, SharedAuthScheme,
424        SharedAuthSchemeOptionResolver, Sign,
425    };
426    use aws_smithy_runtime_api::client::endpoint::SharedEndpointResolver;
427    use aws_smithy_runtime_api::client::identity::{
428        Identity, IdentityFuture, ResolveIdentity, SharedIdentityResolver,
429    };
430    use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
431    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
432    use aws_smithy_runtime_api::client::runtime_components::{
433        GetIdentityResolver, RuntimeComponents, RuntimeComponentsBuilder,
434    };
435    use aws_smithy_types::config_bag::Layer;
436    use std::collections::HashMap;
437
438    #[tokio::test]
439    async fn basic_case() {
440        #[derive(Debug)]
441        struct TestIdentityResolver;
442        impl ResolveIdentity for TestIdentityResolver {
443            fn resolve_identity<'a>(
444                &'a self,
445                _runtime_components: &'a RuntimeComponents,
446                _config_bag: &'a ConfigBag,
447            ) -> IdentityFuture<'a> {
448                IdentityFuture::ready(Ok(Identity::new("doesntmatter", None)))
449            }
450        }
451
452        #[derive(Debug)]
453        struct TestSigner;
454
455        impl Sign for TestSigner {
456            fn sign_http_request(
457                &self,
458                request: &mut HttpRequest,
459                _identity: &Identity,
460                _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
461                _runtime_components: &RuntimeComponents,
462                _config_bag: &ConfigBag,
463            ) -> Result<(), BoxError> {
464                request
465                    .headers_mut()
466                    .insert(http_02x::header::AUTHORIZATION, "success!");
467                Ok(())
468            }
469        }
470
471        const TEST_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("test-scheme");
472
473        #[derive(Debug)]
474        struct TestAuthScheme {
475            signer: TestSigner,
476        }
477        impl AuthScheme for TestAuthScheme {
478            fn scheme_id(&self) -> AuthSchemeId {
479                TEST_SCHEME_ID
480            }
481
482            fn identity_resolver(
483                &self,
484                identity_resolvers: &dyn GetIdentityResolver,
485            ) -> Option<SharedIdentityResolver> {
486                identity_resolvers.identity_resolver(self.scheme_id())
487            }
488
489            fn signer(&self) -> &dyn Sign {
490                &self.signer
491            }
492        }
493
494        async fn run_test(add_more_to_layer: impl Fn(Layer) -> Layer) {
495            let mut ctx = InterceptorContext::new(Input::doesnt_matter());
496            ctx.enter_serialization_phase();
497            ctx.set_request(HttpRequest::empty());
498            let _ = ctx.take_input();
499            ctx.enter_before_transmit_phase();
500
501            let runtime_components = RuntimeComponentsBuilder::for_tests()
502                .with_auth_scheme(SharedAuthScheme::new(TestAuthScheme { signer: TestSigner }))
503                .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
504                    StaticAuthSchemeOptionResolver::new(vec![TEST_SCHEME_ID]),
505                )))
506                .with_identity_resolver(
507                    TEST_SCHEME_ID,
508                    SharedIdentityResolver::new(TestIdentityResolver),
509                )
510                .with_endpoint_resolver(Some(SharedEndpointResolver::new(
511                    StaticUriEndpointResolver::http_localhost(8080),
512                )))
513                .build()
514                .unwrap();
515
516            let mut layer: Layer = Layer::new("test");
517            layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
518            layer.store_put(Endpoint::builder().url("dontcare").build());
519            let layer = add_more_to_layer(layer);
520            let mut cfg = ConfigBag::of_layers(vec![layer]);
521
522            let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut cfg)
523                .await
524                .expect("success");
525
526            sign_request(&scheme_id, &identity, &mut ctx, &runtime_components, &cfg)
527                .expect("success");
528
529            assert_eq!(
530                "success!",
531                ctx.request()
532                    .expect("request is set")
533                    .headers()
534                    .get("Authorization")
535                    .unwrap()
536            );
537        }
538
539        // test for correct auth and endpoint orchestration
540        run_test(|mut layer| {
541            layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
542            layer
543        })
544        .await;
545
546        // test for legacy auth and endpoint orchestration (no `AuthSchemeAndEndpointOrchestrationV2` in `layer`)
547        run_test(|mut layer| {
548            layer.store_put(EndpointResolverParams::from(
549                StaticUriEndpointResolverParams::new(),
550            ));
551            layer
552        })
553        .await;
554    }
555
556    #[cfg(feature = "http-auth")]
557    #[tokio::test]
558    async fn select_best_scheme_for_available_identity_resolvers() {
559        use crate::client::auth::http::{BasicAuthScheme, BearerAuthScheme};
560        use aws_smithy_runtime_api::client::auth::http::{
561            HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
562        };
563        use aws_smithy_runtime_api::client::identity::http::{Login, Token};
564
565        async fn run_test(add_more_to_layer: impl Fn(Layer) -> Layer) {
566            let mut ctx = InterceptorContext::new(Input::doesnt_matter());
567            ctx.enter_serialization_phase();
568            ctx.set_request(HttpRequest::empty());
569            let _ = ctx.take_input();
570            ctx.enter_before_transmit_phase();
571
572            // First, test the presence of a basic auth login and absence of a bearer token
573            let (runtime_components, layer) =
574                config_with_identity(HTTP_BASIC_AUTH_SCHEME_ID, Login::new("a", "b", None));
575            let layer = add_more_to_layer(layer);
576            let mut cfg = ConfigBag::of_layers(vec![layer]);
577
578            let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut cfg)
579                .await
580                .expect("success");
581            sign_request(&scheme_id, &identity, &mut ctx, &runtime_components, &cfg)
582                .expect("success");
583            assert_eq!(
584                // "YTpi" == "a:b" in base64
585                "Basic YTpi",
586                ctx.request()
587                    .expect("request is set")
588                    .headers()
589                    .get("Authorization")
590                    .unwrap()
591            );
592
593            // Next, test the presence of a bearer token and absence of basic auth
594            let (runtime_components, layer) =
595                config_with_identity(HTTP_BEARER_AUTH_SCHEME_ID, Token::new("t", None));
596            let layer = add_more_to_layer(layer);
597            let mut cfg = ConfigBag::of_layers(vec![layer]);
598            let mut ctx = InterceptorContext::new(Input::erase("doesnt-matter"));
599            ctx.enter_serialization_phase();
600            ctx.set_request(HttpRequest::empty());
601            let _ = ctx.take_input();
602            ctx.enter_before_transmit_phase();
603            let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut cfg)
604                .await
605                .expect("success");
606            sign_request(&scheme_id, &identity, &mut ctx, &runtime_components, &cfg)
607                .expect("success");
608            assert_eq!(
609                "Bearer t",
610                ctx.request()
611                    .expect("request is set")
612                    .headers()
613                    .get("Authorization")
614                    .unwrap()
615            );
616        }
617
618        fn config_with_identity(
619            scheme_id: AuthSchemeId,
620            identity: impl ResolveIdentity + 'static,
621        ) -> (RuntimeComponents, Layer) {
622            let runtime_components = RuntimeComponentsBuilder::for_tests()
623                .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
624                .with_auth_scheme(SharedAuthScheme::new(BearerAuthScheme::new()))
625                .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
626                    StaticAuthSchemeOptionResolver::new(vec![
627                        HTTP_BASIC_AUTH_SCHEME_ID,
628                        HTTP_BEARER_AUTH_SCHEME_ID,
629                    ]),
630                )))
631                .with_identity_resolver(scheme_id, SharedIdentityResolver::new(identity))
632                .with_endpoint_resolver(Some(SharedEndpointResolver::new(
633                    StaticUriEndpointResolver::http_localhost(8080),
634                )))
635                .build()
636                .unwrap();
637
638            let mut layer = Layer::new("test");
639            layer.store_put(Endpoint::builder().url("dontcare").build());
640            layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
641
642            (runtime_components, layer)
643        }
644
645        // test for correct auth and endpoint orchestration
646        run_test(|mut layer| {
647            layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
648            layer
649        })
650        .await;
651
652        // test for legacy auth and endpoint orchestration (no `AuthSchemeAndEndpointOrchestrationV2` in `layer`)
653        run_test(|mut layer| {
654            layer.store_put(EndpointResolverParams::from(
655                StaticUriEndpointResolverParams::new(),
656            ));
657            layer
658        })
659        .await;
660    }
661
662    #[test]
663    fn extract_endpoint_auth_scheme_config_no_config() {
664        let endpoint = Endpoint::builder()
665            .url("dontcare")
666            .property("something-unrelated", Document::Null)
667            .build();
668        let config =
669            extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
670                .expect("success");
671        assert!(config.as_document().is_none());
672    }
673
674    #[test]
675    fn extract_endpoint_auth_scheme_config_wrong_type() {
676        let endpoint = Endpoint::builder()
677            .url("dontcare")
678            .property("authSchemes", Document::String("bad".into()))
679            .build();
680        extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
681            .expect_err("should fail because authSchemes is the wrong type");
682    }
683
684    #[test]
685    fn extract_endpoint_auth_scheme_config_no_matching_scheme() {
686        let endpoint = Endpoint::builder()
687            .url("dontcare")
688            .property(
689                "authSchemes",
690                vec![
691                    Document::Object({
692                        let mut out = HashMap::new();
693                        out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
694                        out
695                    }),
696                    Document::Object({
697                        let mut out = HashMap::new();
698                        out.insert(
699                            "name".to_string(),
700                            "another-wrong-scheme-id".to_string().into(),
701                        );
702                        out
703                    }),
704                ],
705            )
706            .build();
707        extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
708            .expect_err("should fail because authSchemes doesn't include the desired scheme");
709    }
710
711    #[test]
712    fn extract_endpoint_auth_scheme_config_successfully() {
713        let endpoint = Endpoint::builder()
714            .url("dontcare")
715            .property(
716                "authSchemes",
717                vec![
718                    Document::Object({
719                        let mut out = HashMap::new();
720                        out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
721                        out
722                    }),
723                    Document::Object({
724                        let mut out = HashMap::new();
725                        out.insert("name".to_string(), "test-scheme-id".to_string().into());
726                        out.insert(
727                            "magicString".to_string(),
728                            "magic string value".to_string().into(),
729                        );
730                        out
731                    }),
732                ],
733            )
734            .build();
735        let config =
736            extract_endpoint_auth_scheme_config(&endpoint, &AuthSchemeId::from("test-scheme-id"))
737                .expect("should find test-scheme-id");
738        assert_eq!(
739            "magic string value",
740            config
741                .as_document()
742                .expect("config is set")
743                .as_object()
744                .expect("it's an object")
745                .get("magicString")
746                .expect("magicString is set")
747                .as_string()
748                .expect("gimme the string, dammit!")
749        );
750    }
751
752    #[cfg(feature = "http-auth")]
753    #[tokio::test]
754    async fn use_identity_cache() {
755        use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation};
756        use aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID;
757        use aws_smithy_runtime_api::client::identity::http::Token;
758        use aws_smithy_types::body::SdkBody;
759
760        #[derive(Debug)]
761        struct Cache;
762        impl ResolveCachedIdentity for Cache {
763            fn resolve_cached_identity<'a>(
764                &'a self,
765                _resolver: SharedIdentityResolver,
766                _: &'a RuntimeComponents,
767                _config_bag: &'a ConfigBag,
768            ) -> IdentityFuture<'a> {
769                IdentityFuture::ready(Ok(Identity::new(Token::new("cached (pass)", None), None)))
770            }
771        }
772
773        async fn run_test(add_more_to_layer: impl Fn(Layer) -> Layer) {
774            let mut ctx = InterceptorContext::new(Input::doesnt_matter());
775            ctx.enter_serialization_phase();
776            ctx.set_request(
777                http_02x::Request::builder()
778                    .body(SdkBody::empty())
779                    .unwrap()
780                    .try_into()
781                    .unwrap(),
782            );
783            let _ = ctx.take_input();
784            ctx.enter_before_transmit_phase();
785
786            let runtime_components = RuntimeComponentsBuilder::for_tests()
787                .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
788                    "result:",
789                    ApiKeyLocation::Header,
790                    "Authorization",
791                )))
792                .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
793                    StaticAuthSchemeOptionResolver::new(vec![HTTP_API_KEY_AUTH_SCHEME_ID]),
794                )))
795                .with_identity_cache(Some(Cache))
796                .with_identity_resolver(
797                    HTTP_API_KEY_AUTH_SCHEME_ID,
798                    SharedIdentityResolver::new(Token::new("uncached (fail)", None)),
799                )
800                .with_endpoint_resolver(Some(SharedEndpointResolver::new(
801                    StaticUriEndpointResolver::http_localhost(8080),
802                )))
803                .build()
804                .unwrap();
805            let mut layer = Layer::new("test");
806            layer.store_put(Endpoint::builder().url("dontcare").build());
807            layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
808            let layer = add_more_to_layer(layer);
809            let mut config_bag = ConfigBag::of_layers(vec![layer]);
810
811            let (scheme_id, identity, _) = resolve_identity(&runtime_components, &mut config_bag)
812                .await
813                .expect("success");
814            sign_request(
815                &scheme_id,
816                &identity,
817                &mut ctx,
818                &runtime_components,
819                &config_bag,
820            )
821            .expect("success");
822            assert_eq!(
823                "result: cached (pass)",
824                ctx.request()
825                    .expect("request is set")
826                    .headers()
827                    .get("Authorization")
828                    .unwrap()
829            );
830        }
831
832        // test for correct auth and endpoint orchestration
833        run_test(|mut layer| {
834            layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
835            layer
836        })
837        .await;
838
839        // test for legacy auth and endpoint orchestration (no `AuthSchemeAndEndpointOrchestrationV2` in `layer`)
840        run_test(|mut layer| {
841            layer.store_put(EndpointResolverParams::from(
842                StaticUriEndpointResolverParams::new(),
843            ));
844            layer
845        })
846        .await;
847    }
848
849    #[test]
850    fn friendly_error_messages() {
851        let err = NoMatchingAuthSchemeError(ExploredList::default());
852        assert_eq!(
853            "no auth options are available. This can happen if there's a problem with \
854            the service model, or if there is a codegen bug.",
855            err.to_string()
856        );
857
858        let mut list = ExploredList::default();
859        list.push(
860            AuthSchemeId::new("SigV4"),
861            ExploreResult::NoIdentityResolver,
862        );
863        list.push(
864            AuthSchemeId::new("SigV4a"),
865            ExploreResult::NoIdentityResolver,
866        );
867        let err = NoMatchingAuthSchemeError(list);
868        assert_eq!(
869            "failed to select an auth scheme to sign the request with. \
870            \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
871            \"SigV4a\" wasn't a valid option because there was no identity resolver for it. \
872            Be sure to set an identity, such as credentials, auth token, or other identity \
873            type that is required for this service.",
874            err.to_string()
875        );
876
877        // It should prioritize the suggestion to try an identity before saying it's a bug
878        let mut list = ExploredList::default();
879        list.push(
880            AuthSchemeId::new("SigV4"),
881            ExploreResult::NoIdentityResolver,
882        );
883        list.push(
884            AuthSchemeId::new("SigV4a"),
885            ExploreResult::MissingEndpointConfig,
886        );
887        let err = NoMatchingAuthSchemeError(list);
888        assert_eq!(
889            "failed to select an auth scheme to sign the request with. \
890            \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
891            \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
892            config, but this scheme wasn't listed in it (see \
893            https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
894            Be sure to set an identity, such as credentials, auth token, or other identity \
895            type that is required for this service.",
896            err.to_string()
897        );
898
899        // Otherwise, it should suggest it's a bug
900        let mut list = ExploredList::default();
901        list.push(
902            AuthSchemeId::new("SigV4a"),
903            ExploreResult::MissingEndpointConfig,
904        );
905        let err = NoMatchingAuthSchemeError(list);
906        assert_eq!(
907            "failed to select an auth scheme to sign the request with. \
908            \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
909            config, but this scheme wasn't listed in it (see \
910            https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
911            This is likely a bug.",
912            err.to_string()
913        );
914
915        // Truncation should be indicated
916        let mut list = ExploredList::default();
917        for _ in 0..=MAX_EXPLORED_LIST_LEN {
918            list.push(
919                AuthSchemeId::new("dontcare"),
920                ExploreResult::MissingEndpointConfig,
921            );
922        }
923        let err = NoMatchingAuthSchemeError(list).to_string();
924        if !err.contains(
925            "Note: there were other auth schemes that were evaluated that weren't listed here",
926        ) {
927            panic!("The error should indicate that the explored list was truncated.");
928        }
929    }
930
931    #[cfg(feature = "http-auth")]
932    #[tokio::test]
933    async fn test_resolve_identity() {
934        use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation, BasicAuthScheme};
935        use aws_smithy_runtime_api::client::auth::http::{
936            HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID,
937        };
938        use aws_smithy_runtime_api::client::identity::http::Token;
939        use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
940
941        #[derive(Debug)]
942        struct Cache;
943        impl ResolveCachedIdentity for Cache {
944            fn resolve_cached_identity<'a>(
945                &'a self,
946                identity_resolver: SharedIdentityResolver,
947                rc: &'a RuntimeComponents,
948                cfg: &'a ConfigBag,
949            ) -> IdentityFuture<'a> {
950                IdentityFuture::new(
951                    async move { identity_resolver.resolve_identity(rc, cfg).await },
952                )
953            }
954        }
955
956        let mut layer = Layer::new("test");
957        layer.store_put(AuthSchemeAndEndpointOrchestrationV2);
958        layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
959        let mut cfg = ConfigBag::of_layers(vec![layer]);
960
961        let runtime_components_builder = RuntimeComponentsBuilder::for_tests()
962            .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
963            .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
964                "result:",
965                ApiKeyLocation::Header,
966                "Authorization",
967            )))
968            .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
969                StaticAuthSchemeOptionResolver::new(vec![
970                    HTTP_BASIC_AUTH_SCHEME_ID,
971                    HTTP_API_KEY_AUTH_SCHEME_ID,
972                ]),
973            )))
974            .with_identity_cache(Some(Cache));
975
976        struct TestCase {
977            builder_updater: Box<dyn Fn(RuntimeComponentsBuilder) -> RuntimeComponents>,
978            resolved_auth_scheme: AuthSchemeId,
979            should_error: bool,
980        }
981
982        for test_case in [
983            TestCase {
984                builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
985                    rcb.with_identity_resolver(
986                        HTTP_BASIC_AUTH_SCHEME_ID,
987                        SharedIdentityResolver::new(Token::new("basic", None)),
988                    )
989                    .with_identity_resolver(
990                        HTTP_API_KEY_AUTH_SCHEME_ID,
991                        SharedIdentityResolver::new(Token::new("api-key", None)),
992                    )
993                    .build()
994                    .unwrap()
995                }),
996                resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID,
997                should_error: false,
998            },
999            TestCase {
1000                builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
1001                    rcb.with_identity_resolver(
1002                        HTTP_BASIC_AUTH_SCHEME_ID,
1003                        SharedIdentityResolver::new(Token::new("basic", None)),
1004                    )
1005                    .build()
1006                    .unwrap()
1007                }),
1008                resolved_auth_scheme: HTTP_BASIC_AUTH_SCHEME_ID,
1009                should_error: false,
1010            },
1011            TestCase {
1012                builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| {
1013                    rcb.with_identity_resolver(
1014                        HTTP_API_KEY_AUTH_SCHEME_ID,
1015                        SharedIdentityResolver::new(Token::new("api-key", None)),
1016                    )
1017                    .build()
1018                    .unwrap()
1019                }),
1020                resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID,
1021                should_error: false,
1022            },
1023            TestCase {
1024                builder_updater: Box::new(|rcb: RuntimeComponentsBuilder| rcb.build().unwrap()),
1025                resolved_auth_scheme: HTTP_API_KEY_AUTH_SCHEME_ID,
1026                should_error: true,
1027            },
1028        ]
1029        .into_iter()
1030        {
1031            let runtime_components =
1032                (test_case.builder_updater)(runtime_components_builder.clone());
1033            match resolve_identity(&runtime_components, &mut cfg).await {
1034                Ok(resolved) => assert_eq!(test_case.resolved_auth_scheme, resolved.0),
1035                Err(e) if test_case.should_error => {
1036                    assert!(e.downcast_ref::<NoMatchingAuthSchemeError>().is_some());
1037                }
1038                _ => {
1039                    panic!("`resolve_identity` returned an `Err` when no error was expected in the test.");
1040                }
1041            }
1042        }
1043    }
1044
1045    #[cfg(feature = "http-auth")]
1046    #[tokio::test]
1047    async fn auth_scheme_preference() {
1048        use aws_smithy_runtime_api::client::auth::http::{
1049            HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
1050        };
1051
1052        struct TestCase {
1053            supported: Vec<AuthSchemeOption>,
1054            preference: Option<AuthSchemePreference>,
1055            expected_resolved_auths: Vec<AuthSchemeId>,
1056        }
1057
1058        for test_case in [
1059            TestCase {
1060                supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1061                    .map(AuthSchemeOption::from)
1062                    .to_vec(),
1063                preference: None,
1064                expected_resolved_auths: vec![
1065                    HTTP_API_KEY_AUTH_SCHEME_ID,
1066                    HTTP_BASIC_AUTH_SCHEME_ID,
1067                ],
1068            },
1069            TestCase {
1070                supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1071                    .map(AuthSchemeOption::from)
1072                    .to_vec(),
1073                preference: Some([].into()),
1074                expected_resolved_auths: vec![
1075                    HTTP_API_KEY_AUTH_SCHEME_ID,
1076                    HTTP_BASIC_AUTH_SCHEME_ID,
1077                ],
1078            },
1079            TestCase {
1080                supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1081                    .map(AuthSchemeOption::from)
1082                    .to_vec(),
1083                preference: Some(["bogus"].map(AuthSchemeId::from).into()),
1084                expected_resolved_auths: vec![
1085                    HTTP_API_KEY_AUTH_SCHEME_ID,
1086                    HTTP_BASIC_AUTH_SCHEME_ID,
1087                ],
1088            },
1089            TestCase {
1090                supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1091                    .map(AuthSchemeOption::from)
1092                    .to_vec(),
1093                preference: Some([HTTP_BASIC_AUTH_SCHEME_ID].into()),
1094                expected_resolved_auths: vec![
1095                    HTTP_BASIC_AUTH_SCHEME_ID,
1096                    HTTP_API_KEY_AUTH_SCHEME_ID,
1097                ],
1098            },
1099            TestCase {
1100                supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1101                    .map(AuthSchemeOption::from)
1102                    .to_vec(),
1103                preference: Some([HTTP_BASIC_AUTH_SCHEME_ID, HTTP_API_KEY_AUTH_SCHEME_ID].into()),
1104                expected_resolved_auths: vec![
1105                    HTTP_BASIC_AUTH_SCHEME_ID,
1106                    HTTP_API_KEY_AUTH_SCHEME_ID,
1107                ],
1108            },
1109            TestCase {
1110                supported: [HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID]
1111                    .map(AuthSchemeOption::from)
1112                    .to_vec(),
1113                preference: Some(
1114                    [
1115                        HTTP_BASIC_AUTH_SCHEME_ID,
1116                        HTTP_BEARER_AUTH_SCHEME_ID,
1117                        HTTP_API_KEY_AUTH_SCHEME_ID,
1118                    ]
1119                    .into(),
1120                ),
1121                expected_resolved_auths: vec![
1122                    HTTP_BASIC_AUTH_SCHEME_ID,
1123                    HTTP_API_KEY_AUTH_SCHEME_ID,
1124                ],
1125            },
1126        ] {
1127            let actual = reprioritize_with_auth_scheme_preference(
1128                test_case.supported,
1129                test_case.preference.as_ref(),
1130            )
1131            .await;
1132            let actual = actual
1133                .iter()
1134                .map(|opt| opt.scheme_id())
1135                .cloned()
1136                .collect::<Vec<_>>();
1137            assert_eq!(test_case.expected_resolved_auths, actual);
1138        }
1139    }
1140}