aws_smithy_runtime/client/auth/
http.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Auth scheme implementations for HTTP API Key, Basic Auth, Bearer Token, and Digest auth.
7
8use aws_smithy_http::query_writer::QueryWriter;
9use aws_smithy_runtime_api::box_error::BoxError;
10use aws_smithy_runtime_api::client::auth::http::{
11    HTTP_API_KEY_AUTH_SCHEME_ID, HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
12    HTTP_DIGEST_AUTH_SCHEME_ID,
13};
14use aws_smithy_runtime_api::client::auth::{
15    AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, Sign,
16};
17use aws_smithy_runtime_api::client::identity::http::{Login, Token};
18use aws_smithy_runtime_api::client::identity::{Identity, SharedIdentityResolver};
19use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
20use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
21use aws_smithy_types::base64::encode;
22use aws_smithy_types::config_bag::ConfigBag;
23
24/// Destination for the API key
25#[derive(Copy, Clone, Debug)]
26pub enum ApiKeyLocation {
27    /// Place the API key in the URL query parameters
28    Query,
29    /// Place the API key in the request headers
30    Header,
31}
32
33/// Auth implementation for Smithy's `@httpApiKey` auth scheme
34#[derive(Debug)]
35pub struct ApiKeyAuthScheme {
36    signer: ApiKeySigner,
37}
38
39impl ApiKeyAuthScheme {
40    /// Creates a new `ApiKeyAuthScheme`.
41    pub fn new(
42        scheme: impl Into<String>,
43        location: ApiKeyLocation,
44        name: impl Into<String>,
45    ) -> Self {
46        Self {
47            signer: ApiKeySigner {
48                scheme: scheme.into(),
49                location,
50                name: name.into(),
51            },
52        }
53    }
54}
55
56impl AuthScheme for ApiKeyAuthScheme {
57    fn scheme_id(&self) -> AuthSchemeId {
58        HTTP_API_KEY_AUTH_SCHEME_ID
59    }
60
61    fn identity_resolver(
62        &self,
63        identity_resolvers: &dyn GetIdentityResolver,
64    ) -> Option<SharedIdentityResolver> {
65        identity_resolvers.identity_resolver(self.scheme_id())
66    }
67
68    fn signer(&self) -> &dyn Sign {
69        &self.signer
70    }
71}
72
73#[derive(Debug)]
74struct ApiKeySigner {
75    scheme: String,
76    location: ApiKeyLocation,
77    name: String,
78}
79
80impl Sign for ApiKeySigner {
81    fn sign_http_request(
82        &self,
83        request: &mut HttpRequest,
84        identity: &Identity,
85        _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
86        _runtime_components: &RuntimeComponents,
87        _config_bag: &ConfigBag,
88    ) -> Result<(), BoxError> {
89        let api_key = identity
90            .data::<Token>()
91            .ok_or("HTTP ApiKey auth requires a `Token` identity")?;
92        match self.location {
93            ApiKeyLocation::Header => {
94                request
95                    .headers_mut()
96                    .try_append(
97                        self.name.to_ascii_lowercase(),
98                        format!("{} {}", self.scheme, api_key.token()),
99                    )
100                    .map_err(|_| {
101                        "API key contains characters that can't be included in a HTTP header"
102                    })?;
103            }
104            ApiKeyLocation::Query => {
105                let mut query = QueryWriter::new_from_string(request.uri())?;
106                query.insert(&self.name, api_key.token());
107                request
108                    .set_uri(query.build_uri())
109                    .expect("query writer returns a valid URI")
110            }
111        }
112
113        Ok(())
114    }
115}
116
117/// Auth implementation for Smithy's `@httpBasicAuth` auth scheme
118#[derive(Debug, Default)]
119pub struct BasicAuthScheme {
120    signer: BasicAuthSigner,
121}
122
123impl BasicAuthScheme {
124    /// Creates a new `BasicAuthScheme`.
125    pub fn new() -> Self {
126        Self {
127            signer: BasicAuthSigner,
128        }
129    }
130}
131
132impl AuthScheme for BasicAuthScheme {
133    fn scheme_id(&self) -> AuthSchemeId {
134        HTTP_BASIC_AUTH_SCHEME_ID
135    }
136
137    fn identity_resolver(
138        &self,
139        identity_resolvers: &dyn GetIdentityResolver,
140    ) -> Option<SharedIdentityResolver> {
141        identity_resolvers.identity_resolver(self.scheme_id())
142    }
143
144    fn signer(&self) -> &dyn Sign {
145        &self.signer
146    }
147}
148
149#[derive(Debug, Default)]
150struct BasicAuthSigner;
151
152impl Sign for BasicAuthSigner {
153    fn sign_http_request(
154        &self,
155        request: &mut HttpRequest,
156        identity: &Identity,
157        _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
158        _runtime_components: &RuntimeComponents,
159        _config_bag: &ConfigBag,
160    ) -> Result<(), BoxError> {
161        let login = identity
162            .data::<Login>()
163            .ok_or("HTTP basic auth requires a `Login` identity")?;
164        request.headers_mut().insert(
165            http_02x::header::AUTHORIZATION,
166            http_02x::HeaderValue::from_str(&format!(
167                "Basic {}",
168                encode(format!("{}:{}", login.user(), login.password()))
169            ))
170            .expect("valid header value"),
171        );
172        Ok(())
173    }
174}
175
176/// Auth implementation for Smithy's `@httpBearerAuth` auth scheme
177#[derive(Debug, Default)]
178pub struct BearerAuthScheme {
179    signer: BearerAuthSigner,
180}
181
182impl BearerAuthScheme {
183    /// Creates a new `BearerAuthScheme`.
184    pub fn new() -> Self {
185        Self {
186            signer: BearerAuthSigner,
187        }
188    }
189}
190
191impl AuthScheme for BearerAuthScheme {
192    fn scheme_id(&self) -> AuthSchemeId {
193        HTTP_BEARER_AUTH_SCHEME_ID
194    }
195
196    fn identity_resolver(
197        &self,
198        identity_resolvers: &dyn GetIdentityResolver,
199    ) -> Option<SharedIdentityResolver> {
200        identity_resolvers.identity_resolver(self.scheme_id())
201    }
202
203    fn signer(&self) -> &dyn Sign {
204        &self.signer
205    }
206}
207
208#[derive(Debug, Default)]
209struct BearerAuthSigner;
210
211impl Sign for BearerAuthSigner {
212    fn sign_http_request(
213        &self,
214        request: &mut HttpRequest,
215        identity: &Identity,
216        _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
217        _runtime_components: &RuntimeComponents,
218        _config_bag: &ConfigBag,
219    ) -> Result<(), BoxError> {
220        let token = identity
221            .data::<Token>()
222            .ok_or("HTTP bearer auth requires a `Token` identity")?;
223        request.headers_mut().insert(
224            http_02x::header::AUTHORIZATION,
225            http_02x::HeaderValue::from_str(&format!("Bearer {}", token.token())).map_err(
226                |_| "Bearer token contains characters that can't be included in a HTTP header",
227            )?,
228        );
229        Ok(())
230    }
231}
232
233/// Auth implementation for Smithy's `@httpDigestAuth` auth scheme
234#[derive(Debug, Default)]
235pub struct DigestAuthScheme {
236    signer: DigestAuthSigner,
237}
238
239impl DigestAuthScheme {
240    /// Creates a new `DigestAuthScheme`.
241    pub fn new() -> Self {
242        Self {
243            signer: DigestAuthSigner,
244        }
245    }
246}
247
248impl AuthScheme for DigestAuthScheme {
249    fn scheme_id(&self) -> AuthSchemeId {
250        HTTP_DIGEST_AUTH_SCHEME_ID
251    }
252
253    fn identity_resolver(
254        &self,
255        identity_resolvers: &dyn GetIdentityResolver,
256    ) -> Option<SharedIdentityResolver> {
257        identity_resolvers.identity_resolver(self.scheme_id())
258    }
259
260    fn signer(&self) -> &dyn Sign {
261        &self.signer
262    }
263}
264
265#[derive(Debug, Default)]
266struct DigestAuthSigner;
267
268impl Sign for DigestAuthSigner {
269    fn sign_http_request(
270        &self,
271        _request: &mut HttpRequest,
272        _identity: &Identity,
273        _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
274        _runtime_components: &RuntimeComponents,
275        _config_bag: &ConfigBag,
276    ) -> Result<(), BoxError> {
277        unimplemented!(
278            "support for signing with Smithy's `@httpDigestAuth` auth scheme is not implemented yet"
279        )
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use aws_smithy_runtime_api::client::identity::http::Login;
287    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
288    use aws_smithy_types::body::SdkBody;
289
290    #[test]
291    fn test_api_key_signing_headers() {
292        let signer = ApiKeySigner {
293            scheme: "SomeSchemeName".into(),
294            location: ApiKeyLocation::Header,
295            name: "some-header-name".into(),
296        };
297        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
298        let config_bag = ConfigBag::base();
299        let identity = Identity::new(Token::new("some-token", None), None);
300        let mut request: HttpRequest = http_02x::Request::builder()
301            .uri("http://example.com/Foobaz")
302            .body(SdkBody::empty())
303            .unwrap()
304            .try_into()
305            .unwrap();
306        signer
307            .sign_http_request(
308                &mut request,
309                &identity,
310                AuthSchemeEndpointConfig::empty(),
311                &runtime_components,
312                &config_bag,
313            )
314            .expect("success");
315        assert_eq!(
316            "SomeSchemeName some-token",
317            request.headers().get("some-header-name").unwrap()
318        );
319        assert_eq!("http://example.com/Foobaz", request.uri().to_string());
320    }
321
322    #[test]
323    fn test_api_key_signing_query() {
324        let signer = ApiKeySigner {
325            scheme: "".into(),
326            location: ApiKeyLocation::Query,
327            name: "some-query-name".into(),
328        };
329        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
330        let config_bag = ConfigBag::base();
331        let identity = Identity::new(Token::new("some-token", None), None);
332        let mut request: HttpRequest = http_02x::Request::builder()
333            .uri("http://example.com/Foobaz")
334            .body(SdkBody::empty())
335            .unwrap()
336            .try_into()
337            .unwrap();
338        signer
339            .sign_http_request(
340                &mut request,
341                &identity,
342                AuthSchemeEndpointConfig::empty(),
343                &runtime_components,
344                &config_bag,
345            )
346            .expect("success");
347        assert!(request.headers().get("some-query-name").is_none());
348        assert_eq!(
349            "http://example.com/Foobaz?some-query-name=some-token",
350            request.uri().to_string()
351        );
352    }
353
354    #[test]
355    fn test_basic_auth() {
356        let signer = BasicAuthSigner;
357        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
358        let config_bag = ConfigBag::base();
359        let identity = Identity::new(Login::new("Aladdin", "open sesame", None), None);
360        let mut request = http_02x::Request::builder()
361            .body(SdkBody::empty())
362            .unwrap()
363            .try_into()
364            .unwrap();
365
366        signer
367            .sign_http_request(
368                &mut request,
369                &identity,
370                AuthSchemeEndpointConfig::empty(),
371                &runtime_components,
372                &config_bag,
373            )
374            .expect("success");
375        assert_eq!(
376            "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
377            request.headers().get("Authorization").unwrap()
378        );
379    }
380
381    #[test]
382    fn test_bearer_auth() {
383        let signer = BearerAuthSigner;
384
385        let config_bag = ConfigBag::base();
386        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
387        let identity = Identity::new(Token::new("some-token", None), None);
388        let mut request = http_02x::Request::builder()
389            .body(SdkBody::empty())
390            .unwrap()
391            .try_into()
392            .unwrap();
393        signer
394            .sign_http_request(
395                &mut request,
396                &identity,
397                AuthSchemeEndpointConfig::empty(),
398                &runtime_components,
399                &config_bag,
400            )
401            .expect("success");
402        assert_eq!(
403            "Bearer some-token",
404            request.headers().get("Authorization").unwrap()
405        );
406    }
407
408    #[test]
409    fn test_bearer_auth_overwrite_existing_header() {
410        let signer = BearerAuthSigner;
411
412        let config_bag = ConfigBag::base();
413        let runtime_components = RuntimeComponentsBuilder::for_tests().build().unwrap();
414        let identity = Identity::new(Token::new("some-token", None), None);
415        let mut request = http_02x::Request::builder()
416            .header("Authorization", "wrong")
417            .body(SdkBody::empty())
418            .unwrap()
419            .try_into()
420            .unwrap();
421        signer
422            .sign_http_request(
423                &mut request,
424                &identity,
425                AuthSchemeEndpointConfig::empty(),
426                &runtime_components,
427                &config_bag,
428            )
429            .expect("success");
430        assert_eq!(
431            "Bearer some-token",
432            request.headers().get("Authorization").unwrap()
433        );
434    }
435}