aws_config/sso/
credentials.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! SSO Credentials Provider
7//!
8//! This credentials provider enables loading credentials from `~/.aws/sso/cache`. For more information,
9//! see [Using AWS SSO Credentials](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/sso-credentials.html)
10//!
11//! This provider is included automatically when profiles are loaded.
12
13use super::cache::load_cached_token;
14use crate::identity::IdentityCache;
15use crate::provider_config::ProviderConfig;
16use crate::sso::SsoTokenProvider;
17use aws_credential_types::credential_feature::AwsCredentialFeature;
18use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
19use aws_credential_types::Credentials;
20use aws_sdk_sso::types::RoleCredentials;
21use aws_sdk_sso::Client as SsoClient;
22use aws_smithy_async::time::SharedTimeSource;
23use aws_smithy_types::DateTime;
24use aws_types::os_shim_internal::{Env, Fs};
25use aws_types::region::Region;
26use aws_types::SdkConfig;
27
28/// SSO Credentials Provider
29///
30/// _Note: This provider is part of the default credentials chain and is integrated with the profile-file provider._
31///
32/// This credentials provider will use cached SSO tokens stored in `~/.aws/sso/cache/<hash>.json`.
33/// Two different values will be tried for `<hash>` in order:
34/// 1. The configured [`session_name`](Builder::session_name).
35/// 2. The configured [`start_url`](Builder::start_url).
36#[derive(Debug)]
37pub struct SsoCredentialsProvider {
38    fs: Fs,
39    env: Env,
40    sso_provider_config: SsoProviderConfig,
41    sdk_config: SdkConfig,
42    token_provider: Option<SsoTokenProvider>,
43    time_source: SharedTimeSource,
44}
45
46impl SsoCredentialsProvider {
47    /// Creates a builder for [`SsoCredentialsProvider`]
48    pub fn builder() -> Builder {
49        Builder::new()
50    }
51
52    pub(crate) fn new(
53        provider_config: &ProviderConfig,
54        sso_provider_config: SsoProviderConfig,
55    ) -> Self {
56        let fs = provider_config.fs();
57        let env = provider_config.env();
58
59        let token_provider = if let Some(session_name) = &sso_provider_config.session_name {
60            Some(
61                SsoTokenProvider::builder()
62                    .configure(&provider_config.client_config())
63                    .start_url(&sso_provider_config.start_url)
64                    .session_name(session_name)
65                    .region(sso_provider_config.region.clone())
66                    .build_with(env.clone(), fs.clone()),
67            )
68        } else {
69            None
70        };
71
72        SsoCredentialsProvider {
73            fs,
74            env,
75            sso_provider_config,
76            sdk_config: provider_config.client_config(),
77            token_provider,
78            time_source: provider_config.time_source(),
79        }
80    }
81
82    async fn credentials(&self) -> provider::Result {
83        load_sso_credentials(
84            &self.sso_provider_config,
85            &self.sdk_config,
86            self.token_provider.as_ref(),
87            &self.env,
88            &self.fs,
89            self.time_source.clone(),
90        )
91        .await
92        .map(|mut creds| {
93            creds
94                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
95                .push(AwsCredentialFeature::CredentialsSso);
96            creds
97        })
98    }
99}
100
101impl ProvideCredentials for SsoCredentialsProvider {
102    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
103    where
104        Self: 'a,
105    {
106        future::ProvideCredentials::new(self.credentials())
107    }
108}
109
110/// Builder for [`SsoCredentialsProvider`]
111#[derive(Default, Debug, Clone)]
112pub struct Builder {
113    provider_config: Option<ProviderConfig>,
114    account_id: Option<String>,
115    region: Option<Region>,
116    role_name: Option<String>,
117    start_url: Option<String>,
118    session_name: Option<String>,
119}
120
121impl Builder {
122    /// Create a new builder for [`SsoCredentialsProvider`]
123    pub fn new() -> Self {
124        Self::default()
125    }
126
127    /// Override the configuration used for this provider
128    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
129        self.provider_config = Some(provider_config.clone());
130        self
131    }
132
133    /// Set the account id used for SSO
134    ///
135    /// This is a required field.
136    pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
137        self.account_id = Some(account_id.into());
138        self
139    }
140
141    /// Set the account id used for SSO
142    ///
143    /// This is a required field.
144    pub fn set_account_id(&mut self, account_id: Option<String>) -> &mut Self {
145        self.account_id = account_id;
146        self
147    }
148
149    /// Set the region used for SSO
150    ///
151    /// This is a required field.
152    pub fn region(mut self, region: Region) -> Self {
153        self.region = Some(region);
154        self
155    }
156
157    /// Set the region used for SSO
158    ///
159    /// This is a required field.
160    pub fn set_region(&mut self, region: Option<Region>) -> &mut Self {
161        self.region = region;
162        self
163    }
164
165    /// Set the role name used for SSO
166    ///
167    /// This is a required field.
168    pub fn role_name(mut self, role_name: impl Into<String>) -> Self {
169        self.role_name = Some(role_name.into());
170        self
171    }
172
173    /// Set the role name used for SSO
174    ///
175    /// This is a required field.
176    pub fn set_role_name(&mut self, role_name: Option<String>) -> &mut Self {
177        self.role_name = role_name;
178        self
179    }
180
181    /// Set the start URL used for SSO
182    ///
183    /// This is a required field.
184    pub fn start_url(mut self, start_url: impl Into<String>) -> Self {
185        self.start_url = Some(start_url.into());
186        self
187    }
188
189    /// Set the start URL used for SSO
190    ///
191    /// This is a required field.
192    pub fn set_start_url(&mut self, start_url: Option<String>) -> &mut Self {
193        self.start_url = start_url;
194        self
195    }
196
197    /// Set the session name used for SSO
198    pub fn session_name(mut self, session_name: impl Into<String>) -> Self {
199        self.session_name = Some(session_name.into());
200        self
201    }
202
203    /// Set the session name used for SSO
204    pub fn set_session_name(&mut self, session_name: Option<String>) -> &mut Self {
205        self.session_name = session_name;
206        self
207    }
208
209    /// Construct an SsoCredentialsProvider from the builder
210    ///
211    /// # Panics
212    /// This method will panic if the any of the following required fields are unset:
213    /// - [`start_url`](Self::start_url)
214    /// - [`role_name`](Self::role_name)
215    /// - [`account_id`](Self::account_id)
216    /// - [`region`](Self::region)
217    pub fn build(self) -> SsoCredentialsProvider {
218        let provider_config = self.provider_config.unwrap_or_default();
219        let sso_config = SsoProviderConfig {
220            account_id: self.account_id.expect("account_id must be set"),
221            region: self.region.expect("region must be set"),
222            role_name: self.role_name.expect("role_name must be set"),
223            start_url: self.start_url.expect("start_url must be set"),
224            session_name: self.session_name,
225        };
226        SsoCredentialsProvider::new(&provider_config, sso_config)
227    }
228}
229
230#[derive(Debug)]
231pub(crate) struct SsoProviderConfig {
232    pub(crate) account_id: String,
233    pub(crate) role_name: String,
234    pub(crate) start_url: String,
235    pub(crate) region: Region,
236    pub(crate) session_name: Option<String>,
237}
238
239async fn load_sso_credentials(
240    sso_provider_config: &SsoProviderConfig,
241    sdk_config: &SdkConfig,
242    token_provider: Option<&SsoTokenProvider>,
243    env: &Env,
244    fs: &Fs,
245    time_source: SharedTimeSource,
246) -> provider::Result {
247    let token = if let Some(token_provider) = token_provider {
248        token_provider
249            .resolve_token(time_source)
250            .await
251            .map_err(CredentialsError::provider_error)?
252    } else {
253        // Backwards compatible token loading that uses `start_url` instead of `session_name`
254        load_cached_token(env, fs, &sso_provider_config.start_url)
255            .await
256            .map_err(CredentialsError::provider_error)?
257    };
258
259    let config = sdk_config
260        .to_builder()
261        .region(sso_provider_config.region.clone())
262        .identity_cache(IdentityCache::no_cache())
263        .build();
264    // TODO(enableNewSmithyRuntimeCleanup): Use `customize().config_override()` to set the region instead of creating a new client once middleware is removed
265    let client = SsoClient::new(&config);
266    let resp = client
267        .get_role_credentials()
268        .role_name(&sso_provider_config.role_name)
269        .access_token(&*token.access_token)
270        .account_id(&sso_provider_config.account_id)
271        .send()
272        .await
273        .map_err(CredentialsError::provider_error)?;
274    let credentials: RoleCredentials = resp
275        .role_credentials
276        .ok_or_else(|| CredentialsError::unhandled("SSO did not return credentials"))?;
277    let akid = credentials
278        .access_key_id
279        .ok_or_else(|| CredentialsError::unhandled("no access key id in response"))?;
280    let secret_key = credentials
281        .secret_access_key
282        .ok_or_else(|| CredentialsError::unhandled("no secret key in response"))?;
283    let expiration = DateTime::from_millis(credentials.expiration)
284        .try_into()
285        .map_err(|err| {
286            CredentialsError::unhandled(format!(
287                "expiration could not be converted into a system time: {}",
288                err
289            ))
290        })?;
291    let mut builder = Credentials::builder()
292        .access_key_id(akid)
293        .secret_access_key(secret_key)
294        .account_id(&sso_provider_config.account_id)
295        .expiry(expiration)
296        .provider_name("SSO");
297    builder.set_session_token(credentials.session_token);
298    Ok(builder.build())
299}