Skip to main content

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!("${ENV_VAR_TOKEN_FILE} was not set"))
134                })?;
135                let role_arn = env.get(ENV_VAR_ROLE_ARN).map_err(|_| {
136                    CredentialsError::not_loaded("AWS_ROLE_ARN environment variable must be set")
137                })?;
138                let session_name = env.get(ENV_VAR_SESSION_NAME).unwrap_or_else(|_| {
139                    sts::util::default_session_name("web-identity-token", self.time_source.now())
140                });
141                Ok(Cow::Owned(StaticConfiguration {
142                    web_identity_token_file: token_file.into(),
143                    role_arn,
144                    session_name,
145                }))
146            }
147            Source::Static(conf) => Ok(Cow::Borrowed(conf)),
148        }
149    }
150    async fn credentials(&self) -> provider::Result {
151        let conf = self.source()?;
152        load_credentials(
153            &self.fs,
154            &self.sts_client,
155            self.policy.clone(),
156            self.policy_arns.clone(),
157            &conf.web_identity_token_file,
158            &conf.role_arn,
159            &conf.session_name,
160        )
161        .await
162        .map(|mut creds| {
163            creds
164                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
165                .push(AwsCredentialFeature::CredentialsProfileStsWebIdToken);
166            creds
167        })
168    }
169}
170
171/// Builder for [`WebIdentityTokenCredentialsProvider`].
172#[derive(Debug, Default)]
173pub struct Builder {
174    source: Option<Source>,
175    config: Option<ProviderConfig>,
176    policy: Option<String>,
177    policy_arns: Option<Vec<PolicyDescriptorType>>,
178}
179
180impl Builder {
181    /// Configure generic options of the [WebIdentityTokenCredentialsProvider]
182    ///
183    /// # Examples
184    /// ```no_run
185    /// # async fn test() {
186    /// use aws_config::web_identity_token::WebIdentityTokenCredentialsProvider;
187    /// use aws_config::provider_config::ProviderConfig;
188    /// let provider = WebIdentityTokenCredentialsProvider::builder()
189    ///     .configure(&ProviderConfig::with_default_region().await)
190    ///     .build();
191    /// # }
192    /// ```
193    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
194        self.config = Some(provider_config.clone());
195        self
196    }
197
198    /// Configure this builder to use  [`StaticConfiguration`].
199    ///
200    /// WebIdentityToken providers load credentials from the file system. The file system path used
201    /// may either determine be loaded from environment variables (default), or via a statically
202    /// configured path.
203    pub fn static_configuration(mut self, config: StaticConfiguration) -> Self {
204        self.source = Some(Source::Static(config));
205        self
206    }
207
208    /// Set an IAM policy in JSON format that you want to use as an inline session policy.
209    ///
210    /// This parameter is optional
211    /// For more information, see
212    /// [policy](aws_sdk_sts::operation::assume_role::builders::AssumeRoleInputBuilder::policy_arns)
213    pub fn policy(mut self, policy: impl Into<String>) -> Self {
214        self.policy = Some(policy.into());
215        self
216    }
217
218    /// Set the Amazon Resource Names (ARNs) of the IAM managed policies that you want to use as managed session policies.
219    ///
220    /// This parameter is optional.
221    /// For more information, see
222    /// [policy_arns](aws_sdk_sts::operation::assume_role::builders::AssumeRoleInputBuilder::policy_arns)
223    pub fn policy_arns(mut self, policy_arns: Vec<String>) -> Self {
224        self.policy_arns = Some(
225            policy_arns
226                .into_iter()
227                .map(|arn| PolicyDescriptorType::builder().arn(arn).build())
228                .collect::<Vec<_>>(),
229        );
230        self
231    }
232
233    /// Build a [`WebIdentityTokenCredentialsProvider`]
234    ///
235    /// ## Panics
236    /// If no connector has been enabled via crate features and no connector has been provided via the
237    /// builder, this function will panic.
238    pub fn build(self) -> WebIdentityTokenCredentialsProvider {
239        let conf = self.config.unwrap_or_default();
240        let source = self.source.unwrap_or_else(|| Source::Env(conf.env()));
241        WebIdentityTokenCredentialsProvider {
242            source,
243            fs: conf.fs(),
244            sts_client: StsClient::new(&conf.client_config()),
245            time_source: conf.time_source(),
246            policy: self.policy,
247            policy_arns: self.policy_arns,
248        }
249    }
250}
251
252async fn load_credentials(
253    fs: &Fs,
254    sts_client: &StsClient,
255    policy: Option<String>,
256    policy_arns: Option<Vec<PolicyDescriptorType>>,
257    token_file: impl AsRef<Path>,
258    role_arn: &str,
259    session_name: &str,
260) -> provider::Result {
261    let token = fs
262        .read_to_end(token_file)
263        .await
264        .map_err(CredentialsError::provider_error)?;
265    let token = String::from_utf8(token).map_err(|_utf_8_error| {
266        CredentialsError::unhandled("WebIdentityToken was not valid UTF-8")
267    })?;
268
269    let resp = sts_client.assume_role_with_web_identity()
270        .role_arn(role_arn)
271        .role_session_name(session_name)
272        .set_policy(policy)
273        .set_policy_arns(policy_arns)
274        .web_identity_token(token)
275        .send()
276        .await
277        .map_err(|sdk_error| {
278            tracing::warn!(error = %DisplayErrorContext(&sdk_error), "STS returned an error assuming web identity role");
279            CredentialsError::provider_error(sdk_error)
280        })?;
281    sts::util::into_credentials(resp.credentials, resp.assumed_role_user, "WebIdentityToken")
282}
283
284#[cfg(test)]
285mod test {
286    use crate::provider_config::ProviderConfig;
287    use crate::test_case::no_traffic_client;
288    use crate::web_identity_token::{
289        Builder, ENV_VAR_ROLE_ARN, ENV_VAR_SESSION_NAME, ENV_VAR_TOKEN_FILE,
290    };
291    use aws_credential_types::provider::error::CredentialsError;
292    use aws_smithy_async::rt::sleep::TokioSleep;
293    use aws_smithy_types::error::display::DisplayErrorContext;
294    use aws_types::os_shim_internal::{Env, Fs};
295    use aws_types::region::Region;
296    use std::collections::HashMap;
297
298    #[tokio::test]
299    async fn unloaded_provider() {
300        // empty environment
301        let conf = ProviderConfig::empty()
302            .with_sleep_impl(TokioSleep::new())
303            .with_env(Env::from_slice(&[]))
304            .with_http_client(no_traffic_client())
305            .with_region(Some(Region::from_static("us-east-1")));
306
307        let provider = Builder::default().configure(&conf).build();
308        let err = provider
309            .credentials()
310            .await
311            .expect_err("should fail, provider not loaded");
312        match err {
313            CredentialsError::CredentialsNotLoaded { .. } => { /* ok */ }
314            _ => panic!("incorrect error variant"),
315        }
316    }
317
318    #[tokio::test]
319    async fn missing_env_var() {
320        let env = Env::from_slice(&[(ENV_VAR_TOKEN_FILE, "/token.jwt")]);
321        let region = Some(Region::new("us-east-1"));
322        let provider = Builder::default()
323            .configure(
324                &ProviderConfig::empty()
325                    .with_sleep_impl(TokioSleep::new())
326                    .with_region(region)
327                    .with_env(env)
328                    .with_http_client(no_traffic_client()),
329            )
330            .build();
331        let err = provider
332            .credentials()
333            .await
334            .expect_err("should fail, provider not loaded");
335        assert!(
336            format!("{}", DisplayErrorContext(&err)).contains("AWS_ROLE_ARN"),
337            "`{}` did not contain expected string",
338            err
339        );
340        assert!(
341            matches!(err, CredentialsError::CredentialsNotLoaded(_)),
342            "incorrect error variant: {err:?}"
343        );
344    }
345
346    #[tokio::test]
347    async fn fs_missing_file() {
348        let env = Env::from_slice(&[
349            (ENV_VAR_TOKEN_FILE, "/token.jwt"),
350            (ENV_VAR_ROLE_ARN, "arn:aws:iam::123456789123:role/test-role"),
351            (ENV_VAR_SESSION_NAME, "test-session"),
352        ]);
353        let fs = Fs::from_raw_map(HashMap::new());
354        let provider = Builder::default()
355            .configure(
356                &ProviderConfig::empty()
357                    .with_sleep_impl(TokioSleep::new())
358                    .with_http_client(no_traffic_client())
359                    .with_region(Some(Region::new("us-east-1")))
360                    .with_env(env)
361                    .with_fs(fs),
362            )
363            .build();
364        let err = provider.credentials().await.expect_err("no JWT token");
365        match err {
366            CredentialsError::ProviderError { .. } => { /* ok */ }
367            _ => panic!("incorrect error variant"),
368        }
369    }
370}