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