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