1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
2# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"). You
5# may not use this file except in compliance with the License. A copy of
6# the License is located at
7#
8# http://aws.amazon.com/apache2.0/
9#
10# or in the "license" file accompanying this file. This file is
11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12# ANY KIND, either express or implied. See the License for the specific
13# language governing permissions and limitations under the License.
14import time
15import datetime
16import logging
17import os
18import getpass
19import threading
20import json
21import subprocess
22from collections import namedtuple
23from copy import deepcopy
24from hashlib import sha1
25
26from dateutil.parser import parse
27from dateutil.tz import tzlocal, tzutc
28
29import botocore.configloader
30import botocore.compat
31from botocore import UNSIGNED
32from botocore.compat import total_seconds
33from botocore.compat import compat_shell_split
34from botocore.config import Config
35from botocore.exceptions import UnknownCredentialError
36from botocore.exceptions import PartialCredentialsError
37from botocore.exceptions import ConfigNotFound
38from botocore.exceptions import InvalidConfigError
39from botocore.exceptions import InfiniteLoopConfigError
40from botocore.exceptions import RefreshWithMFAUnsupportedError
41from botocore.exceptions import MetadataRetrievalError
42from botocore.exceptions import CredentialRetrievalError
43from botocore.exceptions import UnauthorizedSSOTokenError
44from botocore.utils import InstanceMetadataFetcher, parse_key_val_file
45from botocore.utils import ContainerMetadataFetcher
46from botocore.utils import FileWebIdentityTokenLoader
47from botocore.utils import SSOTokenLoader
48
49
50logger = logging.getLogger(__name__)
51ReadOnlyCredentials = namedtuple('ReadOnlyCredentials',
52                                 ['access_key', 'secret_key', 'token'])
53
54
55def create_credential_resolver(session, cache=None, region_name=None):
56    """Create a default credential resolver.
57
58    This creates a pre-configured credential resolver
59    that includes the default lookup chain for
60    credentials.
61
62    """
63    profile_name = session.get_config_variable('profile') or 'default'
64    metadata_timeout = session.get_config_variable('metadata_service_timeout')
65    num_attempts = session.get_config_variable('metadata_service_num_attempts')
66    disable_env_vars = session.instance_variables().get('profile') is not None
67
68    if cache is None:
69        cache = {}
70
71    env_provider = EnvProvider()
72    container_provider = ContainerProvider()
73    instance_metadata_provider = InstanceMetadataProvider(
74        iam_role_fetcher=InstanceMetadataFetcher(
75            timeout=metadata_timeout,
76            num_attempts=num_attempts,
77            user_agent=session.user_agent())
78    )
79
80    profile_provider_builder = ProfileProviderBuilder(
81        session, cache=cache, region_name=region_name)
82    assume_role_provider = AssumeRoleProvider(
83        load_config=lambda: session.full_config,
84        client_creator=_get_client_creator(session, region_name),
85        cache=cache,
86        profile_name=profile_name,
87        credential_sourcer=CanonicalNameCredentialSourcer([
88            env_provider, container_provider, instance_metadata_provider
89        ]),
90        profile_provider_builder=profile_provider_builder,
91    )
92
93    pre_profile = [
94        env_provider,
95        assume_role_provider,
96    ]
97    profile_providers = profile_provider_builder.providers(
98        profile_name=profile_name,
99        disable_env_vars=disable_env_vars,
100    )
101    post_profile = [
102        OriginalEC2Provider(),
103        BotoProvider(),
104        container_provider,
105        instance_metadata_provider,
106    ]
107    providers = pre_profile + profile_providers + post_profile
108
109    if disable_env_vars:
110        # An explicitly provided profile will negate an EnvProvider.
111        # We will defer to providers that understand the "profile"
112        # concept to retrieve credentials.
113        # The one edge case if is all three values are provided via
114        # env vars:
115        # export AWS_ACCESS_KEY_ID=foo
116        # export AWS_SECRET_ACCESS_KEY=bar
117        # export AWS_PROFILE=baz
118        # Then, just like our client() calls, the explicit credentials
119        # will take precedence.
120        #
121        # This precedence is enforced by leaving the EnvProvider in the chain.
122        # This means that the only way a "profile" would win is if the
123        # EnvProvider does not return credentials, which is what we want
124        # in this scenario.
125        providers.remove(env_provider)
126        logger.debug('Skipping environment variable credential check'
127                     ' because profile name was explicitly set.')
128
129    resolver = CredentialResolver(providers=providers)
130    return resolver
131
132
133class ProfileProviderBuilder(object):
134    """This class handles the creation of profile based providers.
135
136    NOTE: This class is only intended for internal use.
137
138    This class handles the creation and ordering of the various credential
139    providers that primarly source their configuration from the shared config.
140    This is needed to enable sharing between the default credential chain and
141    the source profile chain created by the assume role provider.
142    """
143    def __init__(self, session, cache=None, region_name=None,
144                 sso_token_cache=None):
145        self._session = session
146        self._cache = cache
147        self._region_name = region_name
148        self._sso_token_cache = sso_token_cache
149
150    def providers(self, profile_name, disable_env_vars=False):
151        return [
152            self._create_web_identity_provider(
153                profile_name, disable_env_vars,
154            ),
155            self._create_sso_provider(profile_name),
156            self._create_shared_credential_provider(profile_name),
157            self._create_process_provider(profile_name),
158            self._create_config_provider(profile_name),
159        ]
160
161    def _create_process_provider(self, profile_name):
162        return ProcessProvider(
163            profile_name=profile_name,
164            load_config=lambda: self._session.full_config,
165        )
166
167    def _create_shared_credential_provider(self, profile_name):
168        credential_file = self._session.get_config_variable('credentials_file')
169        return SharedCredentialProvider(
170            profile_name=profile_name,
171            creds_filename=credential_file,
172        )
173
174    def _create_config_provider(self, profile_name):
175        config_file = self._session.get_config_variable('config_file')
176        return ConfigProvider(
177            profile_name=profile_name,
178            config_filename=config_file,
179        )
180
181    def _create_web_identity_provider(self, profile_name, disable_env_vars):
182        return AssumeRoleWithWebIdentityProvider(
183            load_config=lambda: self._session.full_config,
184            client_creator=_get_client_creator(
185                self._session, self._region_name),
186            cache=self._cache,
187            profile_name=profile_name,
188            disable_env_vars=disable_env_vars,
189        )
190
191    def _create_sso_provider(self, profile_name):
192        return SSOProvider(
193            load_config=lambda: self._session.full_config,
194            client_creator=self._session.create_client,
195            profile_name=profile_name,
196            cache=self._cache,
197            token_cache=self._sso_token_cache,
198        )
199
200
201def get_credentials(session):
202    resolver = create_credential_resolver(session)
203    return resolver.load_credentials()
204
205
206def _local_now():
207    return datetime.datetime.now(tzlocal())
208
209
210def _parse_if_needed(value):
211    if isinstance(value, datetime.datetime):
212        return value
213    return parse(value)
214
215
216def _serialize_if_needed(value, iso=False):
217    if isinstance(value, datetime.datetime):
218        if iso:
219            return value.isoformat()
220        return value.strftime('%Y-%m-%dT%H:%M:%S%Z')
221    return value
222
223
224def _get_client_creator(session, region_name):
225    def client_creator(service_name, **kwargs):
226        create_client_kwargs = {
227            'region_name': region_name
228        }
229        create_client_kwargs.update(**kwargs)
230        return session.create_client(service_name, **create_client_kwargs)
231
232    return client_creator
233
234
235def create_assume_role_refresher(client, params):
236    def refresh():
237        response = client.assume_role(**params)
238        credentials = response['Credentials']
239        # We need to normalize the credential names to
240        # the values expected by the refresh creds.
241        return {
242            'access_key': credentials['AccessKeyId'],
243            'secret_key': credentials['SecretAccessKey'],
244            'token': credentials['SessionToken'],
245            'expiry_time': _serialize_if_needed(credentials['Expiration']),
246        }
247    return refresh
248
249
250def create_mfa_serial_refresher(actual_refresh):
251
252    class _Refresher(object):
253        def __init__(self, refresh):
254            self._refresh = refresh
255            self._has_been_called = False
256
257        def __call__(self):
258            if self._has_been_called:
259                # We can explore an option in the future to support
260                # reprompting for MFA, but for now we just error out
261                # when the temp creds expire.
262                raise RefreshWithMFAUnsupportedError()
263            self._has_been_called = True
264            return self._refresh()
265
266    return _Refresher(actual_refresh)
267
268
269class JSONFileCache(object):
270    """JSON file cache.
271    This provides a dict like interface that stores JSON serializable
272    objects.
273    The objects are serialized to JSON and stored in a file.  These
274    values can be retrieved at a later time.
275    """
276
277    CACHE_DIR = os.path.expanduser(os.path.join('~', '.aws', 'boto', 'cache'))
278
279    def __init__(self, working_dir=CACHE_DIR):
280        self._working_dir = working_dir
281
282    def __contains__(self, cache_key):
283        actual_key = self._convert_cache_key(cache_key)
284        return os.path.isfile(actual_key)
285
286    def __getitem__(self, cache_key):
287        """Retrieve value from a cache key."""
288        actual_key = self._convert_cache_key(cache_key)
289        try:
290            with open(actual_key) as f:
291                return json.load(f)
292        except (OSError, ValueError, IOError):
293            raise KeyError(cache_key)
294
295    def __setitem__(self, cache_key, value):
296        full_key = self._convert_cache_key(cache_key)
297        try:
298            file_content = json.dumps(value, default=_serialize_if_needed)
299        except (TypeError, ValueError):
300            raise ValueError("Value cannot be cached, must be "
301                             "JSON serializable: %s" % value)
302        if not os.path.isdir(self._working_dir):
303            os.makedirs(self._working_dir)
304        with os.fdopen(os.open(full_key,
305                               os.O_WRONLY | os.O_CREAT, 0o600), 'w') as f:
306            f.truncate()
307            f.write(file_content)
308
309    def _convert_cache_key(self, cache_key):
310        full_path = os.path.join(self._working_dir, cache_key + '.json')
311        return full_path
312
313
314class Credentials(object):
315    """
316    Holds the credentials needed to authenticate requests.
317
318    :ivar access_key: The access key part of the credentials.
319    :ivar secret_key: The secret key part of the credentials.
320    :ivar token: The security token, valid only for session credentials.
321    :ivar method: A string which identifies where the credentials
322        were found.
323    """
324
325    def __init__(self, access_key, secret_key, token=None,
326                 method=None):
327        self.access_key = access_key
328        self.secret_key = secret_key
329        self.token = token
330
331        if method is None:
332            method = 'explicit'
333        self.method = method
334
335        self._normalize()
336
337    def _normalize(self):
338        # Keys would sometimes (accidentally) contain non-ascii characters.
339        # It would cause a confusing UnicodeDecodeError in Python 2.
340        # We explicitly convert them into unicode to avoid such error.
341        #
342        # Eventually the service will decide whether to accept the credential.
343        # This also complies with the behavior in Python 3.
344        self.access_key = botocore.compat.ensure_unicode(self.access_key)
345        self.secret_key = botocore.compat.ensure_unicode(self.secret_key)
346
347    def get_frozen_credentials(self):
348        return ReadOnlyCredentials(self.access_key,
349                                   self.secret_key,
350                                   self.token)
351
352
353class RefreshableCredentials(Credentials):
354    """
355    Holds the credentials needed to authenticate requests. In addition, it
356    knows how to refresh itself.
357
358    :ivar access_key: The access key part of the credentials.
359    :ivar secret_key: The secret key part of the credentials.
360    :ivar token: The security token, valid only for session credentials.
361    :ivar method: A string which identifies where the credentials
362        were found.
363    """
364    # The time at which we'll attempt to refresh, but not
365    # block if someone else is refreshing.
366    _advisory_refresh_timeout = 15 * 60
367    # The time at which all threads will block waiting for
368    # refreshed credentials.
369    _mandatory_refresh_timeout = 10 * 60
370
371    def __init__(self, access_key, secret_key, token,
372                 expiry_time, refresh_using, method,
373                 time_fetcher=_local_now):
374        self._refresh_using = refresh_using
375        self._access_key = access_key
376        self._secret_key = secret_key
377        self._token = token
378        self._expiry_time = expiry_time
379        self._time_fetcher = time_fetcher
380        self._refresh_lock = threading.Lock()
381        self.method = method
382        self._frozen_credentials = ReadOnlyCredentials(
383            access_key, secret_key, token)
384        self._normalize()
385
386    def _normalize(self):
387        self._access_key = botocore.compat.ensure_unicode(self._access_key)
388        self._secret_key = botocore.compat.ensure_unicode(self._secret_key)
389
390    @classmethod
391    def create_from_metadata(cls, metadata, refresh_using, method):
392        instance = cls(
393            access_key=metadata['access_key'],
394            secret_key=metadata['secret_key'],
395            token=metadata['token'],
396            expiry_time=cls._expiry_datetime(metadata['expiry_time']),
397            method=method,
398            refresh_using=refresh_using
399        )
400        return instance
401
402    @property
403    def access_key(self):
404        """Warning: Using this property can lead to race conditions if you
405        access another property subsequently along the refresh boundary.
406        Please use get_frozen_credentials instead.
407        """
408        self._refresh()
409        return self._access_key
410
411    @access_key.setter
412    def access_key(self, value):
413        self._access_key = value
414
415    @property
416    def secret_key(self):
417        """Warning: Using this property can lead to race conditions if you
418        access another property subsequently along the refresh boundary.
419        Please use get_frozen_credentials instead.
420        """
421        self._refresh()
422        return self._secret_key
423
424    @secret_key.setter
425    def secret_key(self, value):
426        self._secret_key = value
427
428    @property
429    def token(self):
430        """Warning: Using this property can lead to race conditions if you
431        access another property subsequently along the refresh boundary.
432        Please use get_frozen_credentials instead.
433        """
434        self._refresh()
435        return self._token
436
437    @token.setter
438    def token(self, value):
439        self._token = value
440
441    def _seconds_remaining(self):
442        delta = self._expiry_time - self._time_fetcher()
443        return total_seconds(delta)
444
445    def refresh_needed(self, refresh_in=None):
446        """Check if a refresh is needed.
447
448        A refresh is needed if the expiry time associated
449        with the temporary credentials is less than the
450        provided ``refresh_in``.  If ``time_delta`` is not
451        provided, ``self.advisory_refresh_needed`` will be used.
452
453        For example, if your temporary credentials expire
454        in 10 minutes and the provided ``refresh_in`` is
455        ``15 * 60``, then this function will return ``True``.
456
457        :type refresh_in: int
458        :param refresh_in: The number of seconds before the
459            credentials expire in which refresh attempts should
460            be made.
461
462        :return: True if refresh needed, False otherwise.
463
464        """
465        if self._expiry_time is None:
466            # No expiration, so assume we don't need to refresh.
467            return False
468
469        if refresh_in is None:
470            refresh_in = self._advisory_refresh_timeout
471        # The credentials should be refreshed if they're going to expire
472        # in less than 5 minutes.
473        if self._seconds_remaining() >= refresh_in:
474            # There's enough time left. Don't refresh.
475            return False
476        logger.debug("Credentials need to be refreshed.")
477        return True
478
479    def _is_expired(self):
480        # Checks if the current credentials are expired.
481        return self.refresh_needed(refresh_in=0)
482
483    def _refresh(self):
484        # In the common case where we don't need a refresh, we
485        # can immediately exit and not require acquiring the
486        # refresh lock.
487        if not self.refresh_needed(self._advisory_refresh_timeout):
488            return
489
490        # acquire() doesn't accept kwargs, but False is indicating
491        # that we should not block if we can't acquire the lock.
492        # If we aren't able to acquire the lock, we'll trigger
493        # the else clause.
494        if self._refresh_lock.acquire(False):
495            try:
496                if not self.refresh_needed(self._advisory_refresh_timeout):
497                    return
498                is_mandatory_refresh = self.refresh_needed(
499                    self._mandatory_refresh_timeout)
500                self._protected_refresh(is_mandatory=is_mandatory_refresh)
501                return
502            finally:
503                self._refresh_lock.release()
504        elif self.refresh_needed(self._mandatory_refresh_timeout):
505            # If we're within the mandatory refresh window,
506            # we must block until we get refreshed credentials.
507            with self._refresh_lock:
508                if not self.refresh_needed(self._mandatory_refresh_timeout):
509                    return
510                self._protected_refresh(is_mandatory=True)
511
512    def _protected_refresh(self, is_mandatory):
513        # precondition: this method should only be called if you've acquired
514        # the self._refresh_lock.
515        try:
516            metadata = self._refresh_using()
517        except Exception as e:
518            period_name = 'mandatory' if is_mandatory else 'advisory'
519            logger.warning("Refreshing temporary credentials failed "
520                           "during %s refresh period.",
521                           period_name, exc_info=True)
522            if is_mandatory:
523                # If this is a mandatory refresh, then
524                # all errors that occur when we attempt to refresh
525                # credentials are propagated back to the user.
526                raise
527            # Otherwise we'll just return.
528            # The end result will be that we'll use the current
529            # set of temporary credentials we have.
530            return
531        self._set_from_data(metadata)
532        self._frozen_credentials = ReadOnlyCredentials(
533            self._access_key, self._secret_key, self._token)
534        if self._is_expired():
535            # We successfully refreshed credentials but for whatever
536            # reason, our refreshing function returned credentials
537            # that are still expired.  In this scenario, the only
538            # thing we can do is let the user know and raise
539            # an exception.
540            msg = ("Credentials were refreshed, but the "
541                   "refreshed credentials are still expired.")
542            logger.warning(msg)
543            raise RuntimeError(msg)
544
545    @staticmethod
546    def _expiry_datetime(time_str):
547        return parse(time_str)
548
549    def _set_from_data(self, data):
550        expected_keys = ['access_key', 'secret_key', 'token', 'expiry_time']
551        if not data:
552            missing_keys = expected_keys
553        else:
554            missing_keys = [k for k in expected_keys if k not in data]
555
556        if missing_keys:
557            message = "Credential refresh failed, response did not contain: %s"
558            raise CredentialRetrievalError(
559                provider=self.method,
560                error_msg=message % ', '.join(missing_keys),
561            )
562
563        self.access_key = data['access_key']
564        self.secret_key = data['secret_key']
565        self.token = data['token']
566        self._expiry_time = parse(data['expiry_time'])
567        logger.debug("Retrieved credentials will expire at: %s",
568                     self._expiry_time)
569        self._normalize()
570
571    def get_frozen_credentials(self):
572        """Return immutable credentials.
573
574        The ``access_key``, ``secret_key``, and ``token`` properties
575        on this class will always check and refresh credentials if
576        needed before returning the particular credentials.
577
578        This has an edge case where you can get inconsistent
579        credentials.  Imagine this:
580
581            # Current creds are "t1"
582            tmp.access_key  ---> expired? no, so return t1.access_key
583            # ---- time is now expired, creds need refreshing to "t2" ----
584            tmp.secret_key  ---> expired? yes, refresh and return t2.secret_key
585
586        This means we're using the access key from t1 with the secret key
587        from t2.  To fix this issue, you can request a frozen credential object
588        which is guaranteed not to change.
589
590        The frozen credentials returned from this method should be used
591        immediately and then discarded.  The typical usage pattern would
592        be::
593
594            creds = RefreshableCredentials(...)
595            some_code = SomeSignerObject()
596            # I'm about to sign the request.
597            # The frozen credentials are only used for the
598            # duration of generate_presigned_url and will be
599            # immediately thrown away.
600            request = some_code.sign_some_request(
601                with_credentials=creds.get_frozen_credentials())
602            print("Signed request:", request)
603
604        """
605        self._refresh()
606        return self._frozen_credentials
607
608
609class DeferredRefreshableCredentials(RefreshableCredentials):
610    """Refreshable credentials that don't require initial credentials.
611
612    refresh_using will be called upon first access.
613    """
614    def __init__(self, refresh_using, method, time_fetcher=_local_now):
615        self._refresh_using = refresh_using
616        self._access_key = None
617        self._secret_key = None
618        self._token = None
619        self._expiry_time = None
620        self._time_fetcher = time_fetcher
621        self._refresh_lock = threading.Lock()
622        self.method = method
623        self._frozen_credentials = None
624
625    def refresh_needed(self, refresh_in=None):
626        if self._frozen_credentials is None:
627            return True
628        return super(DeferredRefreshableCredentials, self).refresh_needed(
629            refresh_in
630        )
631
632
633class CachedCredentialFetcher(object):
634    DEFAULT_EXPIRY_WINDOW_SECONDS = 60 * 15
635
636    def __init__(self, cache=None, expiry_window_seconds=None):
637        if cache is None:
638            cache = {}
639        self._cache = cache
640        self._cache_key = self._create_cache_key()
641        if expiry_window_seconds is None:
642            expiry_window_seconds = self.DEFAULT_EXPIRY_WINDOW_SECONDS
643        self._expiry_window_seconds = expiry_window_seconds
644
645    def _create_cache_key(self):
646        raise NotImplementedError('_create_cache_key()')
647
648    def _make_file_safe(self, filename):
649        # Replace :, path sep, and / to make it the string filename safe.
650        filename = filename.replace(':', '_').replace(os.path.sep, '_')
651        return filename.replace('/', '_')
652
653    def _get_credentials(self):
654        raise NotImplementedError('_get_credentials()')
655
656    def fetch_credentials(self):
657        return self._get_cached_credentials()
658
659    def _get_cached_credentials(self):
660        """Get up-to-date credentials.
661
662        This will check the cache for up-to-date credentials, calling assume
663        role if none are available.
664        """
665        response = self._load_from_cache()
666        if response is None:
667            response = self._get_credentials()
668            self._write_to_cache(response)
669        else:
670            logger.debug("Credentials for role retrieved from cache.")
671
672        creds = response['Credentials']
673        expiration = _serialize_if_needed(creds['Expiration'], iso=True)
674        return {
675            'access_key': creds['AccessKeyId'],
676            'secret_key': creds['SecretAccessKey'],
677            'token': creds['SessionToken'],
678            'expiry_time': expiration,
679        }
680
681    def _load_from_cache(self):
682        if self._cache_key in self._cache:
683            creds = deepcopy(self._cache[self._cache_key])
684            if not self._is_expired(creds):
685                return creds
686            else:
687                logger.debug(
688                    "Credentials were found in cache, but they are expired."
689                )
690        return None
691
692    def _write_to_cache(self, response):
693        self._cache[self._cache_key] = deepcopy(response)
694
695    def _is_expired(self, credentials):
696        """Check if credentials are expired."""
697        end_time = _parse_if_needed(credentials['Credentials']['Expiration'])
698        seconds = total_seconds(end_time - _local_now())
699        return seconds < self._expiry_window_seconds
700
701
702class BaseAssumeRoleCredentialFetcher(CachedCredentialFetcher):
703    def __init__(self, client_creator, role_arn, extra_args=None,
704                 cache=None, expiry_window_seconds=None):
705        self._client_creator = client_creator
706        self._role_arn = role_arn
707
708        if extra_args is None:
709            self._assume_kwargs = {}
710        else:
711            self._assume_kwargs = deepcopy(extra_args)
712        self._assume_kwargs['RoleArn'] = self._role_arn
713
714        self._role_session_name = self._assume_kwargs.get('RoleSessionName')
715        self._using_default_session_name = False
716        if not self._role_session_name:
717            self._generate_assume_role_name()
718
719        super(BaseAssumeRoleCredentialFetcher, self).__init__(
720            cache, expiry_window_seconds
721        )
722
723    def _generate_assume_role_name(self):
724        self._role_session_name = 'botocore-session-%s' % (int(time.time()))
725        self._assume_kwargs['RoleSessionName'] = self._role_session_name
726        self._using_default_session_name = True
727
728    def _create_cache_key(self):
729        """Create a predictable cache key for the current configuration.
730
731        The cache key is intended to be compatible with file names.
732        """
733        args = deepcopy(self._assume_kwargs)
734
735        # The role session name gets randomly generated, so we don't want it
736        # in the hash.
737        if self._using_default_session_name:
738            del args['RoleSessionName']
739
740        if 'Policy' in args:
741            # To have a predictable hash, the keys of the policy must be
742            # sorted, so we have to load it here to make sure it gets sorted
743            # later on.
744            args['Policy'] = json.loads(args['Policy'])
745
746        args = json.dumps(args, sort_keys=True)
747        argument_hash = sha1(args.encode('utf-8')).hexdigest()
748        return self._make_file_safe(argument_hash)
749
750
751class AssumeRoleCredentialFetcher(BaseAssumeRoleCredentialFetcher):
752    def __init__(self, client_creator, source_credentials, role_arn,
753                 extra_args=None, mfa_prompter=None, cache=None,
754                 expiry_window_seconds=None):
755        """
756        :type client_creator: callable
757        :param client_creator: A callable that creates a client taking
758            arguments like ``Session.create_client``.
759
760        :type source_credentials: Credentials
761        :param source_credentials: The credentials to use to create the
762            client for the call to AssumeRole.
763
764        :type role_arn: str
765        :param role_arn: The ARN of the role to be assumed.
766
767        :type extra_args: dict
768        :param extra_args: Any additional arguments to add to the assume
769            role request using the format of the botocore operation.
770            Possible keys include, but may not be limited to,
771            DurationSeconds, Policy, SerialNumber, ExternalId and
772            RoleSessionName.
773
774        :type mfa_prompter: callable
775        :param mfa_prompter: A callable that returns input provided by the
776            user (i.e raw_input, getpass.getpass, etc.).
777
778        :type cache: dict
779        :param cache: An object that supports ``__getitem__``,
780            ``__setitem__``, and ``__contains__``.  An example of this is
781            the ``JSONFileCache`` class in aws-cli.
782
783        :type expiry_window_seconds: int
784        :param expiry_window_seconds: The amount of time, in seconds,
785        """
786        self._source_credentials = source_credentials
787        self._mfa_prompter = mfa_prompter
788        if self._mfa_prompter is None:
789            self._mfa_prompter = getpass.getpass
790
791        super(AssumeRoleCredentialFetcher, self).__init__(
792            client_creator, role_arn, extra_args=extra_args,
793            cache=cache, expiry_window_seconds=expiry_window_seconds
794        )
795
796    def _get_credentials(self):
797        """Get credentials by calling assume role."""
798        kwargs = self._assume_role_kwargs()
799        client = self._create_client()
800        return client.assume_role(**kwargs)
801
802    def _assume_role_kwargs(self):
803        """Get the arguments for assume role based on current configuration."""
804        assume_role_kwargs = deepcopy(self._assume_kwargs)
805
806        mfa_serial = assume_role_kwargs.get('SerialNumber')
807
808        if mfa_serial is not None:
809            prompt = 'Enter MFA code for %s: ' % mfa_serial
810            token_code = self._mfa_prompter(prompt)
811            assume_role_kwargs['TokenCode'] = token_code
812
813        duration_seconds = assume_role_kwargs.get('DurationSeconds')
814
815        if duration_seconds is not None:
816            assume_role_kwargs['DurationSeconds'] = duration_seconds
817
818        return assume_role_kwargs
819
820    def _create_client(self):
821        """Create an STS client using the source credentials."""
822        frozen_credentials = self._source_credentials.get_frozen_credentials()
823        return self._client_creator(
824            'sts',
825            aws_access_key_id=frozen_credentials.access_key,
826            aws_secret_access_key=frozen_credentials.secret_key,
827            aws_session_token=frozen_credentials.token,
828        )
829
830
831class AssumeRoleWithWebIdentityCredentialFetcher(
832        BaseAssumeRoleCredentialFetcher
833):
834    def __init__(self, client_creator, web_identity_token_loader, role_arn,
835                 extra_args=None, cache=None, expiry_window_seconds=None):
836        """
837        :type client_creator: callable
838        :param client_creator: A callable that creates a client taking
839            arguments like ``Session.create_client``.
840
841        :type web_identity_token_loader: callable
842        :param web_identity_token_loader: A callable that takes no arguments
843        and returns a web identity token str.
844
845        :type role_arn: str
846        :param role_arn: The ARN of the role to be assumed.
847
848        :type extra_args: dict
849        :param extra_args: Any additional arguments to add to the assume
850            role request using the format of the botocore operation.
851            Possible keys include, but may not be limited to,
852            DurationSeconds, Policy, SerialNumber, ExternalId and
853            RoleSessionName.
854
855        :type cache: dict
856        :param cache: An object that supports ``__getitem__``,
857            ``__setitem__``, and ``__contains__``.  An example of this is
858            the ``JSONFileCache`` class in aws-cli.
859
860        :type expiry_window_seconds: int
861        :param expiry_window_seconds: The amount of time, in seconds,
862        """
863        self._web_identity_token_loader = web_identity_token_loader
864
865        super(AssumeRoleWithWebIdentityCredentialFetcher, self).__init__(
866            client_creator, role_arn, extra_args=extra_args,
867            cache=cache, expiry_window_seconds=expiry_window_seconds
868        )
869
870    def _get_credentials(self):
871        """Get credentials by calling assume role."""
872        kwargs = self._assume_role_kwargs()
873        # Assume role with web identity does not require credentials other than
874        # the token, explicitly configure the client to not sign requests.
875        config = Config(signature_version=UNSIGNED)
876        client = self._client_creator('sts', config=config)
877        return client.assume_role_with_web_identity(**kwargs)
878
879    def _assume_role_kwargs(self):
880        """Get the arguments for assume role based on current configuration."""
881        assume_role_kwargs = deepcopy(self._assume_kwargs)
882        identity_token = self._web_identity_token_loader()
883        assume_role_kwargs['WebIdentityToken'] = identity_token
884
885        return assume_role_kwargs
886
887
888class CredentialProvider(object):
889    # A short name to identify the provider within botocore.
890    METHOD = None
891
892    # A name to identify the provider for use in cross-sdk features like
893    # assume role's `credential_source` configuration option. These names
894    # are to be treated in a case-insensitive way. NOTE: any providers not
895    # implemented in botocore MUST prefix their canonical names with
896    # 'custom' or we DO NOT guarantee that it will work with any features
897    # that this provides.
898    CANONICAL_NAME = None
899
900    def __init__(self, session=None):
901        self.session = session
902
903    def load(self):
904        """
905        Loads the credentials from their source & sets them on the object.
906
907        Subclasses should implement this method (by reading from disk, the
908        environment, the network or wherever), returning ``True`` if they were
909        found & loaded.
910
911        If not found, this method should return ``False``, indictating that the
912        ``CredentialResolver`` should fall back to the next available method.
913
914        The default implementation does nothing, assuming the user has set the
915        ``access_key/secret_key/token`` themselves.
916
917        :returns: Whether credentials were found & set
918        :rtype: Credentials
919        """
920        return True
921
922    def _extract_creds_from_mapping(self, mapping, *key_names):
923        found = []
924        for key_name in key_names:
925            try:
926                found.append(mapping[key_name])
927            except KeyError:
928                raise PartialCredentialsError(provider=self.METHOD,
929                                              cred_var=key_name)
930        return found
931
932
933class ProcessProvider(CredentialProvider):
934
935    METHOD = 'custom-process'
936
937    def __init__(self, profile_name, load_config, popen=subprocess.Popen):
938        self._profile_name = profile_name
939        self._load_config = load_config
940        self._loaded_config = None
941        self._popen = popen
942
943    def load(self):
944        credential_process = self._credential_process
945        if credential_process is None:
946            return
947
948        creds_dict = self._retrieve_credentials_using(credential_process)
949        if creds_dict.get('expiry_time') is not None:
950            return RefreshableCredentials.create_from_metadata(
951                creds_dict,
952                lambda: self._retrieve_credentials_using(credential_process),
953                self.METHOD
954            )
955
956        return Credentials(
957            access_key=creds_dict['access_key'],
958            secret_key=creds_dict['secret_key'],
959            token=creds_dict.get('token'),
960            method=self.METHOD
961        )
962
963    def _retrieve_credentials_using(self, credential_process):
964        # We're not using shell=True, so we need to pass the
965        # command and all arguments as a list.
966        process_list = compat_shell_split(credential_process)
967        p = self._popen(process_list,
968                        stdout=subprocess.PIPE,
969                        stderr=subprocess.PIPE)
970        stdout, stderr = p.communicate()
971        if p.returncode != 0:
972            raise CredentialRetrievalError(
973                provider=self.METHOD, error_msg=stderr.decode('utf-8'))
974        parsed = botocore.compat.json.loads(stdout.decode('utf-8'))
975        version = parsed.get('Version', '<Version key not provided>')
976        if version != 1:
977            raise CredentialRetrievalError(
978                provider=self.METHOD,
979                error_msg=("Unsupported version '%s' for credential process "
980                           "provider, supported versions: 1" % version))
981        try:
982            return {
983                'access_key': parsed['AccessKeyId'],
984                'secret_key': parsed['SecretAccessKey'],
985                'token': parsed.get('SessionToken'),
986                'expiry_time': parsed.get('Expiration'),
987            }
988        except KeyError as e:
989            raise CredentialRetrievalError(
990                provider=self.METHOD,
991                error_msg="Missing required key in response: %s" % e
992            )
993
994    @property
995    def _credential_process(self):
996        if self._loaded_config is None:
997            self._loaded_config = self._load_config()
998        profile_config = self._loaded_config.get(
999            'profiles', {}).get(self._profile_name, {})
1000        return profile_config.get('credential_process')
1001
1002
1003class InstanceMetadataProvider(CredentialProvider):
1004    METHOD = 'iam-role'
1005    CANONICAL_NAME = 'Ec2InstanceMetadata'
1006
1007    def __init__(self, iam_role_fetcher):
1008        self._role_fetcher = iam_role_fetcher
1009
1010    def load(self):
1011        fetcher = self._role_fetcher
1012        # We do the first request, to see if we get useful data back.
1013        # If not, we'll pass & move on to whatever's next in the credential
1014        # chain.
1015        metadata = fetcher.retrieve_iam_role_credentials()
1016        if not metadata:
1017            return None
1018        logger.debug('Found credentials from IAM Role: %s',
1019                     metadata['role_name'])
1020        # We manually set the data here, since we already made the request &
1021        # have it. When the expiry is hit, the credentials will auto-refresh
1022        # themselves.
1023        creds = RefreshableCredentials.create_from_metadata(
1024            metadata,
1025            method=self.METHOD,
1026            refresh_using=fetcher.retrieve_iam_role_credentials,
1027        )
1028        return creds
1029
1030
1031class EnvProvider(CredentialProvider):
1032    METHOD = 'env'
1033    CANONICAL_NAME = 'Environment'
1034    ACCESS_KEY = 'AWS_ACCESS_KEY_ID'
1035    SECRET_KEY = 'AWS_SECRET_ACCESS_KEY'
1036    # The token can come from either of these env var.
1037    # AWS_SESSION_TOKEN is what other AWS SDKs have standardized on.
1038    TOKENS = ['AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN']
1039    EXPIRY_TIME = 'AWS_CREDENTIAL_EXPIRATION'
1040
1041    def __init__(self, environ=None, mapping=None):
1042        """
1043
1044        :param environ: The environment variables (defaults to
1045            ``os.environ`` if no value is provided).
1046        :param mapping: An optional mapping of variable names to
1047            environment variable names.  Use this if you want to
1048            change the mapping of access_key->AWS_ACCESS_KEY_ID, etc.
1049            The dict can have up to 3 keys: ``access_key``, ``secret_key``,
1050            ``session_token``.
1051        """
1052        if environ is None:
1053            environ = os.environ
1054        self.environ = environ
1055        self._mapping = self._build_mapping(mapping)
1056
1057    def _build_mapping(self, mapping):
1058        # Mapping of variable name to env var name.
1059        var_mapping = {}
1060        if mapping is None:
1061            # Use the class var default.
1062            var_mapping['access_key'] = self.ACCESS_KEY
1063            var_mapping['secret_key'] = self.SECRET_KEY
1064            var_mapping['token'] = self.TOKENS
1065            var_mapping['expiry_time'] = self.EXPIRY_TIME
1066        else:
1067            var_mapping['access_key'] = mapping.get(
1068                'access_key', self.ACCESS_KEY)
1069            var_mapping['secret_key'] = mapping.get(
1070                'secret_key', self.SECRET_KEY)
1071            var_mapping['token'] = mapping.get(
1072                'token', self.TOKENS)
1073            if not isinstance(var_mapping['token'], list):
1074                var_mapping['token'] = [var_mapping['token']]
1075            var_mapping['expiry_time'] = mapping.get(
1076                'expiry_time', self.EXPIRY_TIME)
1077        return var_mapping
1078
1079    def load(self):
1080        """
1081        Search for credentials in explicit environment variables.
1082        """
1083
1084        access_key = self.environ.get(self._mapping['access_key'], '')
1085
1086        if access_key:
1087            logger.info('Found credentials in environment variables.')
1088            fetcher = self._create_credentials_fetcher()
1089            credentials = fetcher(require_expiry=False)
1090
1091            expiry_time = credentials['expiry_time']
1092            if expiry_time is not None:
1093                expiry_time = parse(expiry_time)
1094                return RefreshableCredentials(
1095                    credentials['access_key'], credentials['secret_key'],
1096                    credentials['token'], expiry_time,
1097                    refresh_using=fetcher, method=self.METHOD
1098                )
1099
1100            return Credentials(
1101                credentials['access_key'], credentials['secret_key'],
1102                credentials['token'], method=self.METHOD
1103            )
1104        else:
1105            return None
1106
1107    def _create_credentials_fetcher(self):
1108        mapping = self._mapping
1109        method = self.METHOD
1110        environ = self.environ
1111
1112        def fetch_credentials(require_expiry=True):
1113            credentials = {}
1114
1115            access_key = environ.get(mapping['access_key'], '')
1116            if not access_key:
1117                raise PartialCredentialsError(
1118                    provider=method, cred_var=mapping['access_key'])
1119            credentials['access_key'] = access_key
1120
1121            secret_key = environ.get(mapping['secret_key'], '')
1122            if not secret_key:
1123                raise PartialCredentialsError(
1124                    provider=method, cred_var=mapping['secret_key'])
1125            credentials['secret_key'] = secret_key
1126
1127            credentials['token'] = None
1128            for token_env_var in mapping['token']:
1129                token = environ.get(token_env_var, '')
1130                if token:
1131                    credentials['token'] = token
1132                    break
1133
1134            credentials['expiry_time'] = None
1135            expiry_time = environ.get(mapping['expiry_time'], '')
1136            if expiry_time:
1137                credentials['expiry_time'] = expiry_time
1138            if require_expiry and not expiry_time:
1139                raise PartialCredentialsError(
1140                    provider=method, cred_var=mapping['expiry_time'])
1141
1142            return credentials
1143
1144        return fetch_credentials
1145
1146
1147class OriginalEC2Provider(CredentialProvider):
1148    METHOD = 'ec2-credentials-file'
1149    CANONICAL_NAME = 'Ec2Config'
1150
1151    CRED_FILE_ENV = 'AWS_CREDENTIAL_FILE'
1152    ACCESS_KEY = 'AWSAccessKeyId'
1153    SECRET_KEY = 'AWSSecretKey'
1154
1155    def __init__(self, environ=None, parser=None):
1156        if environ is None:
1157            environ = os.environ
1158        if parser is None:
1159            parser = parse_key_val_file
1160        self._environ = environ
1161        self._parser = parser
1162
1163    def load(self):
1164        """
1165        Search for a credential file used by original EC2 CLI tools.
1166        """
1167        if 'AWS_CREDENTIAL_FILE' in self._environ:
1168            full_path = os.path.expanduser(
1169                self._environ['AWS_CREDENTIAL_FILE'])
1170            creds = self._parser(full_path)
1171            if self.ACCESS_KEY in creds:
1172                logger.info('Found credentials in AWS_CREDENTIAL_FILE.')
1173                access_key = creds[self.ACCESS_KEY]
1174                secret_key = creds[self.SECRET_KEY]
1175                # EC2 creds file doesn't support session tokens.
1176                return Credentials(access_key, secret_key, method=self.METHOD)
1177        else:
1178            return None
1179
1180
1181class SharedCredentialProvider(CredentialProvider):
1182    METHOD = 'shared-credentials-file'
1183    CANONICAL_NAME = 'SharedCredentials'
1184
1185    ACCESS_KEY = 'aws_access_key_id'
1186    SECRET_KEY = 'aws_secret_access_key'
1187    # Same deal as the EnvProvider above.  Botocore originally supported
1188    # aws_security_token, but the SDKs are standardizing on aws_session_token
1189    # so we support both.
1190    TOKENS = ['aws_security_token', 'aws_session_token']
1191
1192    def __init__(self, creds_filename, profile_name=None, ini_parser=None):
1193        self._creds_filename = creds_filename
1194        if profile_name is None:
1195            profile_name = 'default'
1196        self._profile_name = profile_name
1197        if ini_parser is None:
1198            ini_parser = botocore.configloader.raw_config_parse
1199        self._ini_parser = ini_parser
1200
1201    def load(self):
1202        try:
1203            available_creds = self._ini_parser(self._creds_filename)
1204        except ConfigNotFound:
1205            return None
1206        if self._profile_name in available_creds:
1207            config = available_creds[self._profile_name]
1208            if self.ACCESS_KEY in config:
1209                logger.info("Found credentials in shared credentials file: %s",
1210                            self._creds_filename)
1211                access_key, secret_key = self._extract_creds_from_mapping(
1212                    config, self.ACCESS_KEY, self.SECRET_KEY)
1213                token = self._get_session_token(config)
1214                return Credentials(access_key, secret_key, token,
1215                                   method=self.METHOD)
1216
1217    def _get_session_token(self, config):
1218        for token_envvar in self.TOKENS:
1219            if token_envvar in config:
1220                return config[token_envvar]
1221
1222
1223class ConfigProvider(CredentialProvider):
1224    """INI based config provider with profile sections."""
1225    METHOD = 'config-file'
1226    CANONICAL_NAME = 'SharedConfig'
1227
1228    ACCESS_KEY = 'aws_access_key_id'
1229    SECRET_KEY = 'aws_secret_access_key'
1230    # Same deal as the EnvProvider above.  Botocore originally supported
1231    # aws_security_token, but the SDKs are standardizing on aws_session_token
1232    # so we support both.
1233    TOKENS = ['aws_security_token', 'aws_session_token']
1234
1235    def __init__(self, config_filename, profile_name, config_parser=None):
1236        """
1237
1238        :param config_filename: The session configuration scoped to the current
1239            profile.  This is available via ``session.config``.
1240        :param profile_name: The name of the current profile.
1241        :param config_parser: A config parser callable.
1242
1243        """
1244        self._config_filename = config_filename
1245        self._profile_name = profile_name
1246        if config_parser is None:
1247            config_parser = botocore.configloader.load_config
1248        self._config_parser = config_parser
1249
1250    def load(self):
1251        """
1252        If there is are credentials in the configuration associated with
1253        the session, use those.
1254        """
1255        try:
1256            full_config = self._config_parser(self._config_filename)
1257        except ConfigNotFound:
1258            return None
1259        if self._profile_name in full_config['profiles']:
1260            profile_config = full_config['profiles'][self._profile_name]
1261            if self.ACCESS_KEY in profile_config:
1262                logger.info("Credentials found in config file: %s",
1263                            self._config_filename)
1264                access_key, secret_key = self._extract_creds_from_mapping(
1265                    profile_config, self.ACCESS_KEY, self.SECRET_KEY)
1266                token = self._get_session_token(profile_config)
1267                return Credentials(access_key, secret_key, token,
1268                                   method=self.METHOD)
1269        else:
1270            return None
1271
1272    def _get_session_token(self, profile_config):
1273        for token_name in self.TOKENS:
1274            if token_name in profile_config:
1275                return profile_config[token_name]
1276
1277
1278class BotoProvider(CredentialProvider):
1279    METHOD = 'boto-config'
1280    CANONICAL_NAME = 'Boto2Config'
1281
1282    BOTO_CONFIG_ENV = 'BOTO_CONFIG'
1283    DEFAULT_CONFIG_FILENAMES = ['/etc/boto.cfg', '~/.boto']
1284    ACCESS_KEY = 'aws_access_key_id'
1285    SECRET_KEY = 'aws_secret_access_key'
1286
1287    def __init__(self, environ=None, ini_parser=None):
1288        if environ is None:
1289            environ = os.environ
1290        if ini_parser is None:
1291            ini_parser = botocore.configloader.raw_config_parse
1292        self._environ = environ
1293        self._ini_parser = ini_parser
1294
1295    def load(self):
1296        """
1297        Look for credentials in boto config file.
1298        """
1299        if self.BOTO_CONFIG_ENV in self._environ:
1300            potential_locations = [self._environ[self.BOTO_CONFIG_ENV]]
1301        else:
1302            potential_locations = self.DEFAULT_CONFIG_FILENAMES
1303        for filename in potential_locations:
1304            try:
1305                config = self._ini_parser(filename)
1306            except ConfigNotFound:
1307                # Move on to the next potential config file name.
1308                continue
1309            if 'Credentials' in config:
1310                credentials = config['Credentials']
1311                if self.ACCESS_KEY in credentials:
1312                    logger.info("Found credentials in boto config file: %s",
1313                                filename)
1314                    access_key, secret_key = self._extract_creds_from_mapping(
1315                        credentials, self.ACCESS_KEY, self.SECRET_KEY)
1316                    return Credentials(access_key, secret_key,
1317                                       method=self.METHOD)
1318
1319
1320class AssumeRoleProvider(CredentialProvider):
1321    METHOD = 'assume-role'
1322    # The AssumeRole provider is logically part of the SharedConfig and
1323    # SharedCredentials providers. Since the purpose of the canonical name
1324    # is to provide cross-sdk compatibility, calling code will need to be
1325    # aware that either of those providers should be tied to the AssumeRole
1326    # provider as much as possible.
1327    CANONICAL_NAME = None
1328    ROLE_CONFIG_VAR = 'role_arn'
1329    WEB_IDENTITY_TOKE_FILE_VAR = 'web_identity_token_file'
1330    # Credentials are considered expired (and will be refreshed) once the total
1331    # remaining time left until the credentials expires is less than the
1332    # EXPIRY_WINDOW.
1333    EXPIRY_WINDOW_SECONDS = 60 * 15
1334
1335    def __init__(self, load_config, client_creator, cache, profile_name,
1336                 prompter=getpass.getpass, credential_sourcer=None,
1337                 profile_provider_builder=None):
1338        """
1339        :type load_config: callable
1340        :param load_config: A function that accepts no arguments, and
1341            when called, will return the full configuration dictionary
1342            for the session (``session.full_config``).
1343
1344        :type client_creator: callable
1345        :param client_creator: A factory function that will create
1346            a client when called.  Has the same interface as
1347            ``botocore.session.Session.create_client``.
1348
1349        :type cache: dict
1350        :param cache: An object that supports ``__getitem__``,
1351            ``__setitem__``, and ``__contains__``.  An example
1352            of this is the ``JSONFileCache`` class in the CLI.
1353
1354        :type profile_name: str
1355        :param profile_name: The name of the profile.
1356
1357        :type prompter: callable
1358        :param prompter: A callable that returns input provided
1359            by the user (i.e raw_input, getpass.getpass, etc.).
1360
1361        :type credential_sourcer: CanonicalNameCredentialSourcer
1362        :param credential_sourcer: A credential provider that takes a
1363            configuration, which is used to provide the source credentials
1364            for the STS call.
1365        """
1366        #: The cache used to first check for assumed credentials.
1367        #: This is checked before making the AssumeRole API
1368        #: calls and can be useful if you have short lived
1369        #: scripts and you'd like to avoid calling AssumeRole
1370        #: until the credentials are expired.
1371        self.cache = cache
1372        self._load_config = load_config
1373        # client_creator is a callable that creates function.
1374        # It's basically session.create_client
1375        self._client_creator = client_creator
1376        self._profile_name = profile_name
1377        self._prompter = prompter
1378        # The _loaded_config attribute will be populated from the
1379        # load_config() function once the configuration is actually
1380        # loaded.  The reason we go through all this instead of just
1381        # requiring that the loaded_config be passed to us is to that
1382        # we can defer configuration loaded until we actually try
1383        # to load credentials (as opposed to when the object is
1384        # instantiated).
1385        self._loaded_config = {}
1386        self._credential_sourcer = credential_sourcer
1387        self._profile_provider_builder = profile_provider_builder
1388        self._visited_profiles = [self._profile_name]
1389
1390    def load(self):
1391        self._loaded_config = self._load_config()
1392        profiles = self._loaded_config.get('profiles', {})
1393        profile = profiles.get(self._profile_name, {})
1394        if self._has_assume_role_config_vars(profile):
1395            return self._load_creds_via_assume_role(self._profile_name)
1396
1397    def _has_assume_role_config_vars(self, profile):
1398        return (
1399            self.ROLE_CONFIG_VAR in profile and
1400            # We need to ensure this provider doesn't look at a profile when
1401            # the profile has configuration for web identity. Simply relying on
1402            # the order in the credential chain is insufficient as it doesn't
1403            # prevent the case when we're doing an assume role chain.
1404            self.WEB_IDENTITY_TOKE_FILE_VAR not in profile
1405        )
1406
1407    def _load_creds_via_assume_role(self, profile_name):
1408        role_config = self._get_role_config(profile_name)
1409        source_credentials = self._resolve_source_credentials(
1410            role_config, profile_name
1411        )
1412
1413        extra_args = {}
1414        role_session_name = role_config.get('role_session_name')
1415        if role_session_name is not None:
1416            extra_args['RoleSessionName'] = role_session_name
1417
1418        external_id = role_config.get('external_id')
1419        if external_id is not None:
1420            extra_args['ExternalId'] = external_id
1421
1422        mfa_serial = role_config.get('mfa_serial')
1423        if mfa_serial is not None:
1424            extra_args['SerialNumber'] = mfa_serial
1425
1426        duration_seconds = role_config.get('duration_seconds')
1427        if duration_seconds is not None:
1428            extra_args['DurationSeconds'] = duration_seconds
1429
1430        fetcher = AssumeRoleCredentialFetcher(
1431            client_creator=self._client_creator,
1432            source_credentials=source_credentials,
1433            role_arn=role_config['role_arn'],
1434            extra_args=extra_args,
1435            mfa_prompter=self._prompter,
1436            cache=self.cache,
1437        )
1438        refresher = fetcher.fetch_credentials
1439        if mfa_serial is not None:
1440            refresher = create_mfa_serial_refresher(refresher)
1441
1442        # The initial credentials are empty and the expiration time is set
1443        # to now so that we can delay the call to assume role until it is
1444        # strictly needed.
1445        return DeferredRefreshableCredentials(
1446            method=self.METHOD,
1447            refresh_using=refresher,
1448            time_fetcher=_local_now
1449        )
1450
1451    def _get_role_config(self, profile_name):
1452        """Retrieves and validates the role configuration for the profile."""
1453        profiles = self._loaded_config.get('profiles', {})
1454
1455        profile = profiles[profile_name]
1456        source_profile = profile.get('source_profile')
1457        role_arn = profile['role_arn']
1458        credential_source = profile.get('credential_source')
1459        mfa_serial = profile.get('mfa_serial')
1460        external_id = profile.get('external_id')
1461        role_session_name = profile.get('role_session_name')
1462        duration_seconds = profile.get('duration_seconds')
1463
1464        role_config = {
1465            'role_arn': role_arn,
1466            'external_id': external_id,
1467            'mfa_serial': mfa_serial,
1468            'role_session_name': role_session_name,
1469            'source_profile': source_profile,
1470            'credential_source': credential_source
1471        }
1472
1473        if duration_seconds is not None:
1474          try:
1475            role_config['duration_seconds'] = int(duration_seconds)
1476          except ValueError:
1477            pass
1478
1479        # Either the credential source or the source profile must be
1480        # specified, but not both.
1481        if credential_source is not None and source_profile is not None:
1482            raise InvalidConfigError(
1483                error_msg=(
1484                    'The profile "%s" contains both source_profile and '
1485                    'credential_source.' % profile_name
1486                )
1487            )
1488        elif credential_source is None and source_profile is None:
1489            raise PartialCredentialsError(
1490                provider=self.METHOD,
1491                cred_var='source_profile or credential_source'
1492            )
1493        elif credential_source is not None:
1494            self._validate_credential_source(
1495                profile_name, credential_source)
1496        else:
1497            self._validate_source_profile(profile_name, source_profile)
1498
1499        return role_config
1500
1501    def _validate_credential_source(self, parent_profile, credential_source):
1502        if self._credential_sourcer is None:
1503            raise InvalidConfigError(error_msg=(
1504                'The credential_source "%s" is specified in profile "%s", '
1505                'but no source provider was configured.' % (
1506                    credential_source, parent_profile)
1507            ))
1508        if not self._credential_sourcer.is_supported(credential_source):
1509            raise InvalidConfigError(error_msg=(
1510                'The credential source "%s" referenced in profile "%s" is not '
1511                'valid.' % (credential_source, parent_profile)
1512            ))
1513
1514    def _source_profile_has_credentials(self, profile):
1515        return any([
1516            self._has_static_credentials(profile),
1517            self._has_assume_role_config_vars(profile),
1518        ])
1519
1520    def _validate_source_profile(self, parent_profile_name,
1521                                 source_profile_name):
1522        profiles = self._loaded_config.get('profiles', {})
1523        if source_profile_name not in profiles:
1524            raise InvalidConfigError(
1525                error_msg=(
1526                    'The source_profile "%s" referenced in '
1527                    'the profile "%s" does not exist.' % (
1528                        source_profile_name, parent_profile_name)
1529                )
1530            )
1531
1532        source_profile = profiles[source_profile_name]
1533
1534        # Make sure we aren't going into an infinite loop. If we haven't
1535        # visited the profile yet, we're good.
1536        if source_profile_name not in self._visited_profiles:
1537            return
1538
1539        # If we have visited the profile and the profile isn't simply
1540        # referencing itself, that's an infinite loop.
1541        if source_profile_name != parent_profile_name:
1542            raise InfiniteLoopConfigError(
1543                source_profile=source_profile_name,
1544                visited_profiles=self._visited_profiles
1545            )
1546
1547        # A profile is allowed to reference itself so that it can source
1548        # static credentials and have configuration all in the same
1549        # profile. This will only ever work for the top level assume
1550        # role because the static credentials will otherwise take
1551        # precedence.
1552        if not self._has_static_credentials(source_profile):
1553            raise InfiniteLoopConfigError(
1554                source_profile=source_profile_name,
1555                visited_profiles=self._visited_profiles
1556            )
1557
1558    def _has_static_credentials(self, profile):
1559        static_keys = ['aws_secret_access_key', 'aws_access_key_id']
1560        return any(static_key in profile for static_key in static_keys)
1561
1562    def _resolve_source_credentials(self, role_config, profile_name):
1563        credential_source = role_config.get('credential_source')
1564        if credential_source is not None:
1565            return self._resolve_credentials_from_source(
1566                credential_source, profile_name
1567            )
1568
1569        source_profile = role_config['source_profile']
1570        self._visited_profiles.append(source_profile)
1571        return self._resolve_credentials_from_profile(source_profile)
1572
1573    def _resolve_credentials_from_profile(self, profile_name):
1574        profiles = self._loaded_config.get('profiles', {})
1575        profile = profiles[profile_name]
1576
1577        if self._has_static_credentials(profile) and \
1578                not self._profile_provider_builder:
1579            # This is only here for backwards compatibility. If this provider
1580            # isn't given a profile provider builder we still want to be able
1581            # handle the basic static credential case as we would before the
1582            # provile provider builder parameter was added.
1583            return self._resolve_static_credentials_from_profile(profile)
1584        elif self._has_static_credentials(profile) or \
1585                not self._has_assume_role_config_vars(profile):
1586            profile_providers = self._profile_provider_builder.providers(
1587                profile_name=profile_name,
1588                disable_env_vars=True,
1589            )
1590            profile_chain = CredentialResolver(profile_providers)
1591            credentials = profile_chain.load_credentials()
1592            if credentials is None:
1593                error_message = (
1594                    'The source profile "%s" must have credentials.'
1595                )
1596                raise InvalidConfigError(
1597                    error_msg=error_message % profile_name,
1598                )
1599            return credentials
1600
1601        return self._load_creds_via_assume_role(profile_name)
1602
1603    def _resolve_static_credentials_from_profile(self, profile):
1604        try:
1605            return Credentials(
1606                access_key=profile['aws_access_key_id'],
1607                secret_key=profile['aws_secret_access_key'],
1608                token=profile.get('aws_session_token')
1609            )
1610        except KeyError as e:
1611            raise PartialCredentialsError(
1612                provider=self.METHOD, cred_var=str(e))
1613
1614    def _resolve_credentials_from_source(self, credential_source,
1615                                         profile_name):
1616        credentials = self._credential_sourcer.source_credentials(
1617            credential_source)
1618        if credentials is None:
1619            raise CredentialRetrievalError(
1620                provider=credential_source,
1621                error_msg=(
1622                    'No credentials found in credential_source referenced '
1623                    'in profile %s' % profile_name
1624                )
1625            )
1626        return credentials
1627
1628
1629class AssumeRoleWithWebIdentityProvider(CredentialProvider):
1630    METHOD = 'assume-role-with-web-identity'
1631    CANONICAL_NAME = None
1632    _CONFIG_TO_ENV_VAR = {
1633        'web_identity_token_file': 'AWS_WEB_IDENTITY_TOKEN_FILE',
1634        'role_session_name': 'AWS_ROLE_SESSION_NAME',
1635        'role_arn': 'AWS_ROLE_ARN',
1636    }
1637
1638    def __init__(
1639            self,
1640            load_config,
1641            client_creator,
1642            profile_name,
1643            cache=None,
1644            disable_env_vars=False,
1645            token_loader_cls=None,
1646    ):
1647        self.cache = cache
1648        self._load_config = load_config
1649        self._client_creator = client_creator
1650        self._profile_name = profile_name
1651        self._profile_config = None
1652        self._disable_env_vars = disable_env_vars
1653        if token_loader_cls is None:
1654            token_loader_cls = FileWebIdentityTokenLoader
1655        self._token_loader_cls = token_loader_cls
1656
1657    def load(self):
1658        return self._assume_role_with_web_identity()
1659
1660    def _get_profile_config(self, key):
1661        if self._profile_config is None:
1662            loaded_config = self._load_config()
1663            profiles = loaded_config.get('profiles', {})
1664            self._profile_config = profiles.get(self._profile_name, {})
1665        return self._profile_config.get(key)
1666
1667    def _get_env_config(self, key):
1668        if self._disable_env_vars:
1669            return None
1670        env_key = self._CONFIG_TO_ENV_VAR.get(key)
1671        if env_key and env_key in os.environ:
1672            return os.environ[env_key]
1673        return None
1674
1675    def _get_config(self, key):
1676        env_value = self._get_env_config(key)
1677        if env_value is not None:
1678            return env_value
1679        return self._get_profile_config(key)
1680
1681    def _assume_role_with_web_identity(self):
1682        token_path = self._get_config('web_identity_token_file')
1683        if not token_path:
1684            return None
1685        token_loader = self._token_loader_cls(token_path)
1686
1687        role_arn = self._get_config('role_arn')
1688        if not role_arn:
1689            error_msg = (
1690                'The provided profile or the current environment is '
1691                'configured to assume role with web identity but has no '
1692                'role ARN configured. Ensure that the profile has the role_arn'
1693                'configuration set or the AWS_ROLE_ARN env var is set.'
1694            )
1695            raise InvalidConfigError(error_msg=error_msg)
1696
1697        extra_args = {}
1698        role_session_name = self._get_config('role_session_name')
1699        if role_session_name is not None:
1700            extra_args['RoleSessionName'] = role_session_name
1701
1702        fetcher = AssumeRoleWithWebIdentityCredentialFetcher(
1703            client_creator=self._client_creator,
1704            web_identity_token_loader=token_loader,
1705            role_arn=role_arn,
1706            extra_args=extra_args,
1707            cache=self.cache,
1708        )
1709        # The initial credentials are empty and the expiration time is set
1710        # to now so that we can delay the call to assume role until it is
1711        # strictly needed.
1712        return DeferredRefreshableCredentials(
1713            method=self.METHOD,
1714            refresh_using=fetcher.fetch_credentials,
1715        )
1716
1717
1718class CanonicalNameCredentialSourcer(object):
1719    def __init__(self, providers):
1720        self._providers = providers
1721
1722    def is_supported(self, source_name):
1723        """Validates a given source name.
1724
1725        :type source_name: str
1726        :param source_name: The value of credential_source in the config
1727            file. This is the canonical name of the credential provider.
1728
1729        :rtype: bool
1730        :returns: True if the credential provider is supported,
1731            False otherwise.
1732        """
1733        return source_name in [p.CANONICAL_NAME for p in self._providers]
1734
1735    def source_credentials(self, source_name):
1736        """Loads source credentials based on the provided configuration.
1737
1738        :type source_name: str
1739        :param source_name: The value of credential_source in the config
1740            file. This is the canonical name of the credential provider.
1741
1742        :rtype: Credentials
1743        """
1744        source = self._get_provider(source_name)
1745        if isinstance(source, CredentialResolver):
1746            return source.load_credentials()
1747        return source.load()
1748
1749    def _get_provider(self, canonical_name):
1750        """Return a credential provider by its canonical name.
1751
1752        :type canonical_name: str
1753        :param canonical_name: The canonical name of the provider.
1754
1755        :raises UnknownCredentialError: Raised if no
1756            credential provider by the provided name
1757            is found.
1758        """
1759        provider = self._get_provider_by_canonical_name(canonical_name)
1760
1761        # The AssumeRole provider should really be part of the SharedConfig
1762        # provider rather than being its own thing, but it is not. It is
1763        # effectively part of both the SharedConfig provider and the
1764        # SharedCredentials provider now due to the way it behaves.
1765        # Therefore if we want either of those providers we should return
1766        # the AssumeRole provider with it.
1767        if canonical_name.lower() in ['sharedconfig', 'sharedcredentials']:
1768            assume_role_provider = self._get_provider_by_method('assume-role')
1769            if assume_role_provider is not None:
1770                # The SharedConfig or SharedCredentials provider may not be
1771                # present if it was removed for some reason, but the
1772                # AssumeRole provider could still be present. In that case,
1773                # return the assume role provider by itself.
1774                if provider is None:
1775                    return assume_role_provider
1776
1777                # If both are present, return them both as a
1778                # CredentialResolver so that calling code can treat them as
1779                # a single entity.
1780                return CredentialResolver([assume_role_provider, provider])
1781
1782        if provider is None:
1783            raise UnknownCredentialError(name=canonical_name)
1784
1785        return provider
1786
1787    def _get_provider_by_canonical_name(self, canonical_name):
1788        """Return a credential provider by its canonical name.
1789
1790        This function is strict, it does not attempt to address
1791        compatibility issues.
1792        """
1793        for provider in self._providers:
1794            name = provider.CANONICAL_NAME
1795            # Canonical names are case-insensitive
1796            if name and name.lower() == canonical_name.lower():
1797                return provider
1798
1799    def _get_provider_by_method(self, method):
1800        """Return a credential provider by its METHOD name."""
1801        for provider in self._providers:
1802            if provider.METHOD == method:
1803                return provider
1804
1805
1806class ContainerProvider(CredentialProvider):
1807    METHOD = 'container-role'
1808    CANONICAL_NAME = 'EcsContainer'
1809    ENV_VAR = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'
1810    ENV_VAR_FULL = 'AWS_CONTAINER_CREDENTIALS_FULL_URI'
1811    ENV_VAR_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN'
1812
1813    def __init__(self, environ=None, fetcher=None):
1814        if environ is None:
1815            environ = os.environ
1816        if fetcher is None:
1817            fetcher = ContainerMetadataFetcher()
1818        self._environ = environ
1819        self._fetcher = fetcher
1820
1821    def load(self):
1822        # This cred provider is only triggered if the self.ENV_VAR is set,
1823        # which only happens if you opt into this feature.
1824        if self.ENV_VAR in self._environ or self.ENV_VAR_FULL in self._environ:
1825            return self._retrieve_or_fail()
1826
1827    def _retrieve_or_fail(self):
1828        if self._provided_relative_uri():
1829            full_uri = self._fetcher.full_url(self._environ[self.ENV_VAR])
1830        else:
1831            full_uri = self._environ[self.ENV_VAR_FULL]
1832        headers = self._build_headers()
1833        fetcher = self._create_fetcher(full_uri, headers)
1834        creds = fetcher()
1835        return RefreshableCredentials(
1836            access_key=creds['access_key'],
1837            secret_key=creds['secret_key'],
1838            token=creds['token'],
1839            method=self.METHOD,
1840            expiry_time=_parse_if_needed(creds['expiry_time']),
1841            refresh_using=fetcher,
1842        )
1843
1844    def _build_headers(self):
1845        headers = {}
1846        auth_token = self._environ.get(self.ENV_VAR_AUTH_TOKEN)
1847        if auth_token is not None:
1848            return {
1849                'Authorization': auth_token
1850            }
1851
1852    def _create_fetcher(self, full_uri, headers):
1853        def fetch_creds():
1854            try:
1855                response = self._fetcher.retrieve_full_uri(
1856                    full_uri, headers=headers)
1857            except MetadataRetrievalError as e:
1858                logger.debug("Error retrieving container metadata: %s", e,
1859                             exc_info=True)
1860                raise CredentialRetrievalError(provider=self.METHOD,
1861                                               error_msg=str(e))
1862            return {
1863                'access_key': response['AccessKeyId'],
1864                'secret_key': response['SecretAccessKey'],
1865                'token': response['Token'],
1866                'expiry_time': response['Expiration'],
1867            }
1868
1869        return fetch_creds
1870
1871    def _provided_relative_uri(self):
1872        return self.ENV_VAR in self._environ
1873
1874
1875class CredentialResolver(object):
1876    def __init__(self, providers):
1877        """
1878
1879        :param providers: A list of ``CredentialProvider`` instances.
1880
1881        """
1882        self.providers = providers
1883
1884    def insert_before(self, name, credential_provider):
1885        """
1886        Inserts a new instance of ``CredentialProvider`` into the chain that
1887        will be tried before an existing one.
1888
1889        :param name: The short name of the credentials you'd like to insert the
1890            new credentials before. (ex. ``env`` or ``config``). Existing names
1891            & ordering can be discovered via ``self.available_methods``.
1892        :type name: string
1893
1894        :param cred_instance: An instance of the new ``Credentials`` object
1895            you'd like to add to the chain.
1896        :type cred_instance: A subclass of ``Credentials``
1897        """
1898        try:
1899            offset = [p.METHOD for p in self.providers].index(name)
1900        except ValueError:
1901            raise UnknownCredentialError(name=name)
1902        self.providers.insert(offset, credential_provider)
1903
1904    def insert_after(self, name, credential_provider):
1905        """
1906        Inserts a new type of ``Credentials`` instance into the chain that will
1907        be tried after an existing one.
1908
1909        :param name: The short name of the credentials you'd like to insert the
1910            new credentials after. (ex. ``env`` or ``config``). Existing names
1911            & ordering can be discovered via ``self.available_methods``.
1912        :type name: string
1913
1914        :param cred_instance: An instance of the new ``Credentials`` object
1915            you'd like to add to the chain.
1916        :type cred_instance: A subclass of ``Credentials``
1917        """
1918        offset = self._get_provider_offset(name)
1919        self.providers.insert(offset + 1, credential_provider)
1920
1921    def remove(self, name):
1922        """
1923        Removes a given ``Credentials`` instance from the chain.
1924
1925        :param name: The short name of the credentials instance to remove.
1926        :type name: string
1927        """
1928        available_methods = [p.METHOD for p in self.providers]
1929        if name not in available_methods:
1930            # It's not present. Fail silently.
1931            return
1932
1933        offset = available_methods.index(name)
1934        self.providers.pop(offset)
1935
1936    def get_provider(self, name):
1937        """Return a credential provider by name.
1938
1939        :type name: str
1940        :param name: The name of the provider.
1941
1942        :raises UnknownCredentialError: Raised if no
1943            credential provider by the provided name
1944            is found.
1945        """
1946        return self.providers[self._get_provider_offset(name)]
1947
1948    def _get_provider_offset(self, name):
1949        try:
1950            return [p.METHOD for p in self.providers].index(name)
1951        except ValueError:
1952            raise UnknownCredentialError(name=name)
1953
1954    def load_credentials(self):
1955        """
1956        Goes through the credentials chain, returning the first ``Credentials``
1957        that could be loaded.
1958        """
1959        # First provider to return a non-None response wins.
1960        for provider in self.providers:
1961            logger.debug("Looking for credentials via: %s", provider.METHOD)
1962            creds = provider.load()
1963            if creds is not None:
1964                return creds
1965
1966        # If we got here, no credentials could be found.
1967        # This feels like it should be an exception, but historically, ``None``
1968        # is returned.
1969        #
1970        # +1
1971        # -js
1972        return None
1973
1974
1975class SSOCredentialFetcher(CachedCredentialFetcher):
1976    def __init__(self, start_url, sso_region, role_name, account_id,
1977                 client_creator, token_loader=None, cache=None,
1978                 expiry_window_seconds=None):
1979        self._client_creator = client_creator
1980        self._sso_region = sso_region
1981        self._role_name = role_name
1982        self._account_id = account_id
1983        self._start_url = start_url
1984        self._token_loader = token_loader
1985
1986        super(SSOCredentialFetcher, self).__init__(
1987            cache, expiry_window_seconds
1988        )
1989
1990    def _create_cache_key(self):
1991        """Create a predictable cache key for the current configuration.
1992
1993        The cache key is intended to be compatible with file names.
1994        """
1995        args = {
1996            'startUrl': self._start_url,
1997            'roleName': self._role_name,
1998            'accountId': self._account_id,
1999        }
2000        # NOTE: It would be good to hoist this cache key construction logic
2001        # into the CachedCredentialFetcher class as we should be consistent.
2002        # Unfortunately, the current assume role fetchers that sub class don't
2003        # pass separators resulting in non-minified JSON. In the long term,
2004        # all fetchers should use the below caching scheme.
2005        args = json.dumps(args, sort_keys=True, separators=(',', ':'))
2006        argument_hash = sha1(args.encode('utf-8')).hexdigest()
2007        return self._make_file_safe(argument_hash)
2008
2009    def _parse_timestamp(self, timestamp_ms):
2010        # fromtimestamp expects seconds so: milliseconds / 1000 = seconds
2011        timestamp_seconds = timestamp_ms / 1000.0
2012        timestamp = datetime.datetime.fromtimestamp(timestamp_seconds, tzutc())
2013        return _serialize_if_needed(timestamp)
2014
2015    def _get_credentials(self):
2016        """Get credentials by calling SSO get role credentials."""
2017        config = Config(
2018            signature_version=UNSIGNED,
2019            region_name=self._sso_region,
2020        )
2021        client = self._client_creator('sso', config=config)
2022
2023        kwargs = {
2024            'roleName': self._role_name,
2025            'accountId': self._account_id,
2026            'accessToken': self._token_loader(self._start_url),
2027        }
2028        try:
2029            response = client.get_role_credentials(**kwargs)
2030        except client.exceptions.UnauthorizedException:
2031            raise UnauthorizedSSOTokenError()
2032        credentials = response['roleCredentials']
2033
2034        credentials = {
2035            'ProviderType': 'sso',
2036            'Credentials': {
2037                'AccessKeyId': credentials['accessKeyId'],
2038                'SecretAccessKey': credentials['secretAccessKey'],
2039                'SessionToken': credentials['sessionToken'],
2040                'Expiration': self._parse_timestamp(credentials['expiration']),
2041            }
2042        }
2043        return credentials
2044
2045
2046class SSOProvider(CredentialProvider):
2047    METHOD = 'sso'
2048
2049    _SSO_TOKEN_CACHE_DIR = os.path.expanduser(
2050        os.path.join('~', '.aws', 'sso', 'cache')
2051    )
2052    _SSO_CONFIG_VARS = [
2053        'sso_start_url',
2054        'sso_region',
2055        'sso_role_name',
2056        'sso_account_id',
2057    ]
2058
2059    def __init__(self, load_config, client_creator, profile_name,
2060                 cache=None, token_cache=None):
2061        if token_cache is None:
2062            token_cache = JSONFileCache(self._SSO_TOKEN_CACHE_DIR)
2063        self._token_cache = token_cache
2064        if cache is None:
2065            cache = {}
2066        self.cache = cache
2067        self._load_config = load_config
2068        self._client_creator = client_creator
2069        self._profile_name = profile_name
2070
2071    def _load_sso_config(self):
2072        loaded_config = self._load_config()
2073        profiles = loaded_config.get('profiles', {})
2074        profile_name = self._profile_name
2075        profile_config = profiles.get(self._profile_name, {})
2076
2077        if all(c not in profile_config for c in self._SSO_CONFIG_VARS):
2078            return None
2079
2080        config = {}
2081        missing_config_vars = []
2082        for config_var in self._SSO_CONFIG_VARS:
2083            if config_var in profile_config:
2084                config[config_var] = profile_config[config_var]
2085            else:
2086                missing_config_vars.append(config_var)
2087
2088        if missing_config_vars:
2089            missing = ', '.join(missing_config_vars)
2090            raise InvalidConfigError(
2091                error_msg=(
2092                    'The profile "%s" is configured to use SSO but is missing '
2093                    'required configuration: %s' % (profile_name, missing)
2094                )
2095            )
2096
2097        return config
2098
2099    def load(self):
2100        sso_config = self._load_sso_config()
2101        if not sso_config:
2102            return None
2103
2104        sso_fetcher = SSOCredentialFetcher(
2105            sso_config['sso_start_url'],
2106            sso_config['sso_region'],
2107            sso_config['sso_role_name'],
2108            sso_config['sso_account_id'],
2109            self._client_creator,
2110            token_loader=SSOTokenLoader(cache=self._token_cache),
2111            cache=self.cache,
2112        )
2113
2114        return DeferredRefreshableCredentials(
2115            method=self.METHOD,
2116            refresh_using=sso_fetcher.fetch_credentials,
2117        )
2118