aws_config/
web_identity_token.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Load Credentials from Web Identity Tokens
7//!
8//! Web identity tokens can be loaded from file. The path may be set in one of three ways:
9//! 1. [Environment Variables](#environment-variable-configuration)
10//! 2. [AWS profile](#aws-profile-configuration) defined in `~/.aws/config`
11//! 3. Static configuration via [`static_configuration`](Builder::static_configuration)
12//!
13//! _Note: [WebIdentityTokenCredentialsProvider] is part of the [default provider chain](crate::default_provider).
14//! Unless you need specific behavior or configuration overrides, it is recommended to use the
15//! default chain instead of using this provider directly. This client should be considered a "low level"
16//! client as it does not include caching or profile-file resolution when used in isolation._
17//!
18//! ## Environment Variable Configuration
19//! WebIdentityTokenCredentialProvider will load the following environment variables:
20//! - `AWS_WEB_IDENTITY_TOKEN_FILE`: **required**, location to find the token file containing a JWT token
21//! - `AWS_ROLE_ARN`: **required**, role ARN to assume
22//! - `AWS_ROLE_SESSION_NAME`: **optional**: Session name to use when assuming the role
23//!
24//! ## AWS Profile Configuration
25//! _Note: Configuration of the web identity token provider via a shared profile is only supported
26//! when using the [`ProfileFileCredentialsProvider`](crate::profile::credentials)._
27//!
28//! Web identity token credentials can be loaded from `~/.aws/config` in two ways:
29//! 1. Directly:
30//!   ```ini
31//!   [profile default]
32//!   role_arn = arn:aws:iam::1234567890123:role/RoleA
33//!   web_identity_token_file = /token.jwt
34//!   ```
35//!
36//! 2. As a source profile for another role:
37//!
38//!   ```ini
39//!   [profile default]
40//!   role_arn = arn:aws:iam::123456789:role/RoleA
41//!   source_profile = base
42//!
43//!   [profile base]
44//!   role_arn = arn:aws:iam::123456789012:role/s3-reader
45//!   web_identity_token_file = /token.jwt
46//!   ```
47//!
48//! # Examples
49//! Web Identity Token providers are part of the [default chain](crate::default_provider::credentials).
50//! However, they may be directly constructed if you don't want to use the default provider chain.
51//! Unless overridden with [`static_configuration`](Builder::static_configuration), the provider will
52//! load configuration from environment variables.
53//!
54//! ```no_run
55//! # async fn test() {
56//! use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider;
57//! use aws_config::provider_config::ProviderConfig;
58//! let provider = WebIdentityTokenCredentialsProvider::builder()
59//!     .configure(&ProviderConfig::with_default_region().await)
60//!     .build();
61//! # }
62//! ```
63
64use crate::provider_config::ProviderConfig;
65use crate::sts;
66use aws_credential_types::credential_feature::AwsCredentialFeature;
67use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
68use aws_sdk_sts::{types::PolicyDescriptorType, Client as StsClient};
69use aws_smithy_async::time::SharedTimeSource;
70use aws_smithy_types::error::display::DisplayErrorContext;
71use aws_types::os_shim_internal::{Env, Fs};
72
73use std::borrow::Cow;
74use std::path::{Path, PathBuf};
75
76const ENV_VAR_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE";
77const ENV_VAR_ROLE_ARN: &str = "AWS_ROLE_ARN";
78const ENV_VAR_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME";
79
80/// Credential provider to load credentials from Web Identity Tokens
81///
82/// See Module documentation for more details
83#[derive(Debug)]
84pub struct WebIdentityTokenCredentialsProvider {
85    source: Source,
86    time_source: SharedTimeSource,
87    fs: Fs,
88    sts_client: StsClient,
89    policy: Option<String>,
90    policy_arns: Option<Vec<PolicyDescriptorType>>,
91}
92
93impl WebIdentityTokenCredentialsProvider {
94    /// Builder for this credentials provider
95    pub fn builder() -> Builder {
96        Builder::default()
97    }
98}
99
100#[derive(Debug)]
101enum Source {
102    Env(Env),
103    Static(StaticConfiguration),
104}
105
106/// Statically configured WebIdentityToken configuration
107#[derive(Debug, Clone)]
108pub struct StaticConfiguration {
109    /// Location of the file containing the web identity token
110    pub web_identity_token_file: PathBuf,
111
112    /// RoleArn to assume
113    pub role_arn: String,
114
115    /// Session name to use when assuming the role
116    pub session_name: String,
117}
118
119impl ProvideCredentials for WebIdentityTokenCredentialsProvider {
120    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
121    where
122        Self: 'a,
123    {
124        future::ProvideCredentials::new(self.credentials())
125    }
126}
127
128impl WebIdentityTokenCredentialsProvider {
129    fn source(&self) -> Result<Cow<'_, StaticConfiguration>, CredentialsError> {
130        match &self.source {
131            Source::Env(env) => {
132                let token_file = env.get(ENV_VAR_TOKEN_FILE).map_err(|_| {
133                    CredentialsError::not_loaded(format!("${} was not set", ENV_VAR_TOKEN_FILE))
134                })?;
135                let role_arn = env.get(ENV_VAR_ROLE_ARN).map_err(|_| {
136                    CredentialsError::invalid_configuration(
137                        "AWS_ROLE_ARN environment variable must be set",
138                    )
139                })?;
140                let session_name = env.get(ENV_VAR_SESSION_NAME).unwrap_or_else(|_| {
141                    sts::util::default_session_name("web-identity-token", self.time_source.now())
142                });
143                Ok(Cow::Owned(StaticConfiguration {
144                    web_identity_token_file: token_file.into(),
145                    role_arn,
146                    session_name,
147                }))
148            }
149            Source::Static(conf) => Ok(Cow::Borrowed(conf)),
150        }
151    }
152    async fn credentials(&self) -> provider::Result {
153        let conf = self.source()?;
154        load_credentials(
155            &self.fs,
156            &self.sts_client,
157            self.policy.clone(),
158            self.policy_arns.clone(),
159            &conf.web_identity_token_file,
160            &conf.role_arn,
161            &conf.session_name,
162        )
163        .await
164        .map(|mut creds| {
165            creds
166                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
167                .push(AwsCredentialFeature::CredentialsProfileStsWebIdToken);
168            creds
169        })
170    }
171}
172
173/// Builder for [`WebIdentityTokenCredentialsProvider`].
174#[derive(Debug, Default)]
175pub struct Builder {
176    source: Option<Source>,
177    config: Option<ProviderConfig>,
178    policy: Option<String>,
179    policy_arns: Option<Vec<PolicyDescriptorType>>,
180}
181
182impl Builder {
183    /// Configure generic options of the [WebIdentityTokenCredentialsProvider]
184    ///
185    /// # Examples
186    /// ```no_run
187    /// # async fn test() {
188    /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider;
189    /// use aws_config::provider_config::ProviderConfig;
190    /// let provider = WebIdentityTokenCredentialsProvider::builder()
191    ///     .configure(&ProviderConfig::with_default_region().await)
192    ///     .build();
193    /// # }
194    /// ```
195    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
196        self.config = Some(provider_config.clone());
197        self
198    }
199
200    /// Configure this builder to use  [`StaticConfiguration`].
201    ///
202    /// WebIdentityToken providers load credentials from the file system. The file system path used
203    /// may either determine be loaded from environment variables (default), or via a statically
204    /// configured path.
205    pub fn static_configuration(mut self, config: StaticConfiguration) -> Self {
206        self.source = Some(Source::Static(config));
207        self
208    }
209
210    /// Set an IAM policy in JSON format that you want to use as an inline session policy.
211    ///
212    /// This parameter is optional
213    /// For more information, see
214    /// [policy](aws_sdk_sts::operation::assume_role::builders::AssumeRoleInputBuilder::policy_arns)
215    pub fn policy(mut self, policy: impl Into<String>) -> Self {
216        self.policy = Some(policy.into());
217        self
218    }
219
220    /// Set the Amazon Resource Names (ARNs) of the IAM managed policies that you want to use as managed session policies.
221    ///
222    /// This parameter is optional.
223    /// For more information, see
224    /// [policy_arns](aws_sdk_sts::operation::assume_role::builders::AssumeRoleInputBuilder::policy_arns)
225    pub fn policy_arns(mut self, policy_arns: Vec<String>) -> Self {
226        self.policy_arns = Some(
227            policy_arns
228                .into_iter()
229                .map(|arn| PolicyDescriptorType::builder().arn(arn).build())
230                .collect::<Vec<_>>(),
231        );
232        self
233    }
234
235    /// Build a [`WebIdentityTokenCredentialsProvider`]
236    ///
237    /// ## Panics
238    /// If no connector has been enabled via crate features and no connector has been provided via the
239    /// builder, this function will panic.
240    pub fn build(self) -> WebIdentityTokenCredentialsProvider {
241        let conf = self.config.unwrap_or_default();
242        let source = self.source.unwrap_or_else(|| Source::Env(conf.env()));
243        WebIdentityTokenCredentialsProvider {
244            source,
245            fs: conf.fs(),
246            sts_client: StsClient::new(&conf.client_config()),
247            time_source: conf.time_source(),
248            policy: self.policy,
249            policy_arns: self.policy_arns,
250        }
251    }
252}
253
254async fn load_credentials(
255    fs: &Fs,
256    sts_client: &StsClient,
257    policy: Option<String>,
258    policy_arns: Option<Vec<PolicyDescriptorType>>,
259    token_file: impl AsRef<Path>,
260    role_arn: &str,
261    session_name: &str,
262) -> provider::Result {
263    let token = fs
264        .read_to_end(token_file)
265        .await
266        .map_err(CredentialsError::provider_error)?;
267    let token = String::from_utf8(token).map_err(|_utf_8_error| {
268        CredentialsError::unhandled("WebIdentityToken was not valid UTF-8")
269    })?;
270
271    let resp = sts_client.assume_role_with_web_identity()
272        .role_arn(role_arn)
273        .role_session_name(session_name)
274        .set_policy(policy)
275        .set_policy_arns(policy_arns)
276        .web_identity_token(token)
277        .send()
278        .await
279        .map_err(|sdk_error| {
280            tracing::warn!(error = %DisplayErrorContext(&sdk_error), "STS returned an error assuming web identity role");
281            CredentialsError::provider_error(sdk_error)
282        })?;
283    sts::util::into_credentials(resp.credentials, resp.assumed_role_user, "WebIdentityToken")
284}
285
286#[cfg(test)]
287mod test {
288    use crate::provider_config::ProviderConfig;
289    use crate::test_case::no_traffic_client;
290    use crate::web_identity_token::{
291        Builder, ENV_VAR_ROLE_ARN, ENV_VAR_SESSION_NAME, ENV_VAR_TOKEN_FILE,
292    };
293    use aws_credential_types::provider::error::CredentialsError;
294    use aws_smithy_async::rt::sleep::TokioSleep;
295    use aws_smithy_types::error::display::DisplayErrorContext;
296    use aws_types::os_shim_internal::{Env, Fs};
297    use aws_types::region::Region;
298    use std::collections::HashMap;
299
300    #[tokio::test]
301    async fn unloaded_provider() {
302        // empty environment
303        let conf = ProviderConfig::empty()
304            .with_sleep_impl(TokioSleep::new())
305            .with_env(Env::from_slice(&[]))
306            .with_http_client(no_traffic_client())
307            .with_region(Some(Region::from_static("us-east-1")));
308
309        let provider = Builder::default().configure(&conf).build();
310        let err = provider
311            .credentials()
312            .await
313            .expect_err("should fail, provider not loaded");
314        match err {
315            CredentialsError::CredentialsNotLoaded { .. } => { /* ok */ }
316            _ => panic!("incorrect error variant"),
317        }
318    }
319
320    #[tokio::test]
321    async fn missing_env_var() {
322        let env = Env::from_slice(&[(ENV_VAR_TOKEN_FILE, "/token.jwt")]);
323        let region = Some(Region::new("us-east-1"));
324        let provider = Builder::default()
325            .configure(
326                &ProviderConfig::empty()
327                    .with_sleep_impl(TokioSleep::new())
328                    .with_region(region)
329                    .with_env(env)
330                    .with_http_client(no_traffic_client()),
331            )
332            .build();
333        let err = provider
334            .credentials()
335            .await
336            .expect_err("should fail, provider not loaded");
337        assert!(
338            format!("{}", DisplayErrorContext(&err)).contains("AWS_ROLE_ARN"),
339            "`{}` did not contain expected string",
340            err
341        );
342        match err {
343            CredentialsError::InvalidConfiguration { .. } => { /* ok */ }
344            _ => panic!("incorrect error variant"),
345        }
346    }
347
348    #[tokio::test]
349    async fn fs_missing_file() {
350        let env = Env::from_slice(&[
351            (ENV_VAR_TOKEN_FILE, "/token.jwt"),
352            (ENV_VAR_ROLE_ARN, "arn:aws:iam::123456789123:role/test-role"),
353            (ENV_VAR_SESSION_NAME, "test-session"),
354        ]);
355        let fs = Fs::from_raw_map(HashMap::new());
356        let provider = Builder::default()
357            .configure(
358                &ProviderConfig::empty()
359                    .with_sleep_impl(TokioSleep::new())
360                    .with_http_client(no_traffic_client())
361                    .with_region(Some(Region::new("us-east-1")))
362                    .with_env(env)
363                    .with_fs(fs),
364            )
365            .build();
366        let err = provider.credentials().await.expect_err("no JWT token");
367        match err {
368            CredentialsError::ProviderError { .. } => { /* ok */ }
369            _ => panic!("incorrect error variant"),
370        }
371    }
372}