diff --git a/.changelog/feature-refresh.json b/.changelog/feature-refresh.json new file mode 100644 index 000000000000..946a2fd8950d --- /dev/null +++ b/.changelog/feature-refresh.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "aws-cpp-sdk-core", + "contributor": "kai-ion", + "description": "Add the shared credential refresh lifecycle to built-in credential providers: advisory and mandatory refresh windows, a jittered backoff that keeps serving cached credentials when a refresh fails, and invalidation of credentials rejected with ExpiredToken or InvalidToken" +} diff --git a/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProvider.h b/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProvider.h index a3371ccdab24..8ba4b54b044a 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProvider.h +++ b/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProvider.h @@ -73,6 +73,11 @@ namespace Aws */ virtual AWSCredentials GetAWSCredentials() = 0; + /** + * Invalidate tells a caching provider that a service rejected the credentials with this access key ID. + */ + virtual void Invalidate(const Aws::String& /*rejectedAccessKeyId*/) {} + protected: /** * The default implementation keeps up with the cache times and lets you know if it's time to refresh your internal caching @@ -227,15 +232,16 @@ namespace Aws */ AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + protected: void Reload() override; private: - bool ExpiresSoon() const; - void RefreshIfExpired(); - - std::shared_ptr m_ec2MetadataConfigLoader; - long m_loadFrequencyMs; + std::shared_ptr m_ec2MetadataConfigLoader; }; /** diff --git a/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProviderChain.h b/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProviderChain.h index 98efd8bcaab1..f4a0b2af9ef3 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProviderChain.h +++ b/src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProviderChain.h @@ -29,6 +29,11 @@ namespace Aws */ virtual AWSCredentials GetAWSCredentials() override; + /** + * Invalidate forwards to the provider that supplied the current credentials. + */ + void Invalidate(const Aws::String& accessKeyId) override; + /** * Gets all providers stored in this chain. */ diff --git a/src/aws-cpp-sdk-core/include/aws/core/auth/CrtCredentialsProvider.h b/src/aws-cpp-sdk-core/include/aws/core/auth/CrtCredentialsProvider.h index ddaade76effa..33a2fb687c4e 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/auth/CrtCredentialsProvider.h +++ b/src/aws-cpp-sdk-core/include/aws/core/auth/CrtCredentialsProvider.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -37,6 +38,16 @@ class AWS_CORE_API CrtCredentialsProvider : public AWSCredentialsProvider { */ AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + + protected: + CrtCredentialsProvider(const std::function()>& credentialsProviderFactory, + std::chrono::milliseconds providerFuturesTimeoutMs, Aws::Client::UserAgentFeature userAgentFeature, + const Aws::String& providerName, Aws::Internal::RefreshFailurePolicy failurePolicy); + private: enum class STATE { INITIALIZED, @@ -45,13 +56,14 @@ class AWS_CORE_API CrtCredentialsProvider : public AWSCredentialsProvider { static AWSCredentials ExtractCredentialsFromCrt(const Aws::Crt::Auth::Credentials& crtCredentials); void Reload() override; - void RefreshIfExpired(); std::shared_ptr m_credentialsProvider; AWSCredentials m_credentials; std::chrono::milliseconds m_providerFuturesTimeoutMs; Aws::Client::UserAgentFeature m_userAgentFeature; Aws::String m_providerName; + AWSCredentials RetrieveForCache(); + std::shared_ptr m_cachingProvider; STATE m_state{STATE::NOT_INITIALIZED}; }; } // namespace Auth diff --git a/src/aws-cpp-sdk-core/include/aws/core/auth/GeneralHTTPCredentialsProvider.h b/src/aws-cpp-sdk-core/include/aws/core/auth/GeneralHTTPCredentialsProvider.h index 7744f5d9f185..9e74ade53b9d 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/auth/GeneralHTTPCredentialsProvider.h +++ b/src/aws-cpp-sdk-core/include/aws/core/auth/GeneralHTTPCredentialsProvider.h @@ -89,6 +89,11 @@ namespace Aws */ AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + static const char AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE[]; static const char AWS_CONTAINER_CREDENTIALS_RELATIVE_URI[]; static const char AWS_CONTAINER_CREDENTIALS_FULL_URI[]; @@ -102,16 +107,14 @@ namespace Aws void Reload() override; private: - bool ExpiresSoon() const; - void RefreshIfExpired(); - Aws::String LoadTokenFromFile() const; std::shared_ptr m_ecsCredentialsClient; Aws::String m_authTokenFilePath; - long m_loadFrequencyMs = REFRESH_THRESHOLD; Aws::Auth::AWSCredentials m_credentials; + AWSCredentials RetrieveForCache(); + std::shared_ptr m_cachingProvider; }; // GeneralHTTPCredentialsProvider was previously known as TaskRoleCredentialsProvider or "ECS credentials provider" diff --git a/src/aws-cpp-sdk-core/include/aws/core/auth/ProfileCredentialsProvider.h b/src/aws-cpp-sdk-core/include/aws/core/auth/ProfileCredentialsProvider.h index daaf9e8d17a2..16cb670cb49e 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/auth/ProfileCredentialsProvider.h +++ b/src/aws-cpp-sdk-core/include/aws/core/auth/ProfileCredentialsProvider.h @@ -37,6 +37,11 @@ class AWS_CORE_API ProfileCredentialsProvider : public AWSCredentialsProvider { */ AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + /** * Returns the fullpath of the calculated credentials profile file */ diff --git a/src/aws-cpp-sdk-core/include/aws/core/auth/SSOCredentialsProvider.h b/src/aws-cpp-sdk-core/include/aws/core/auth/SSOCredentialsProvider.h index 2fb5eec70b6f..cf18f3c474d9 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/auth/SSOCredentialsProvider.h +++ b/src/aws-cpp-sdk-core/include/aws/core/auth/SSOCredentialsProvider.h @@ -27,6 +27,11 @@ namespace Aws { */ AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + private: Aws::UniquePtr m_client; Aws::Auth::AWSCredentials m_credentials; @@ -45,9 +50,10 @@ namespace Aws { Aws::Auth::SSOBearerTokenProvider m_bearerTokenProvider; // The client configuration to use std::shared_ptr m_config; + AWSCredentials RetrieveForCache(); + std::shared_ptr m_cachingProvider; void Reload() override; - void RefreshIfExpired(); Aws::String LoadAccessTokenFile(const Aws::String& ssoAccessTokenPath); }; } // namespace Auth diff --git a/src/aws-cpp-sdk-core/include/aws/core/client/AWSClient.h b/src/aws-cpp-sdk-core/include/aws/core/client/AWSClient.h index a03fd99dac21..9e521cfb9541 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/client/AWSClient.h +++ b/src/aws-cpp-sdk-core/include/aws/core/client/AWSClient.h @@ -371,6 +371,10 @@ namespace Aws * return true if signer's clock is adjusted, false otherwise. */ bool AdjustClockSkew(HttpResponseOutcome& outcome, const Aws::Utils::DateTime& timeRequestSent, const Aws::Utils::DateTime& timeResponseReceived, std::chrono::milliseconds attemptSkew) const; + /** + * InvalidateCredentials invalidates the provider's credentials when a service rejects them as expired or invalid. + */ + void InvalidateCredentials(const AWSError& error, const Aws::String& accessKeyId) const; std::shared_ptr StartOneAttemptAsync(const std::shared_ptr& context) const; Aws::Http::HttpClient::AttemptOutcome OnResponseReceivedAsync(const std::shared_ptr& context, std::shared_ptr httpResponse) const; Aws::Http::HttpClient::AttemptOutcome OnAttemptCompleteAsync(const std::shared_ptr& context, HttpResponseOutcome&& outcome) const; diff --git a/src/aws-cpp-sdk-core/include/aws/core/client/CoreErrors.h b/src/aws-cpp-sdk-core/include/aws/core/client/CoreErrors.h index 1f245145b6f8..68faa81c07d6 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/client/CoreErrors.h +++ b/src/aws-cpp-sdk-core/include/aws/core/client/CoreErrors.h @@ -48,6 +48,8 @@ namespace Aws NOT_INITIALIZED = 25, MEMORY_ALLOCATION = 26, NOT_IMPLEMENTED = 27, + EXPIRED_TOKEN = 28, + INVALID_TOKEN = 29, NETWORK_CONNECTION = 99, // General failure to send message to service diff --git a/src/aws-cpp-sdk-core/include/aws/core/config/EC2InstanceProfileConfigLoader.h b/src/aws-cpp-sdk-core/include/aws/core/config/EC2InstanceProfileConfigLoader.h index 4c1e23259bc0..57ecc8bc0323 100644 --- a/src/aws-cpp-sdk-core/include/aws/core/config/EC2InstanceProfileConfigLoader.h +++ b/src/aws-cpp-sdk-core/include/aws/core/config/EC2InstanceProfileConfigLoader.h @@ -19,6 +19,12 @@ namespace Aws class EC2MetadataClient; } + namespace Auth + { + class AWSCredentialsProvider; + class InstanceProfileCredentialsProvider; + } + namespace Config { static const char* const INSTANCE_PROFILE_KEY = "InstanceProfile"; @@ -44,9 +50,15 @@ namespace Aws protected: virtual bool LoadInternal() override; private: + friend class Aws::Auth::InstanceProfileCredentialsProvider; + + bool RetrieveCredentials(); + Aws::Auth::AWSCredentials GetCredentials() const; + void Invalidate(const Aws::String& accessKeyId); + std::shared_ptr m_ec2metadataClient; - int64_t credentialsValidUntilMillis = 0; - int64_t calculateRetryTime() const; + Aws::Auth::AWSCredentials RetrieveForCache(); + std::shared_ptr m_cachingProvider; }; } } diff --git a/src/aws-cpp-sdk-core/include/aws/core/internal/CredentialsCaching.h b/src/aws-cpp-sdk-core/include/aws/core/internal/CredentialsCaching.h new file mode 100644 index 000000000000..43e04a774f27 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/aws/core/internal/CredentialsCaching.h @@ -0,0 +1,322 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ + namespace Internal + { + // Credential Refresh SEP timing values. + const std::chrono::minutes CREDENTIALS_MANDATORY_REFRESH_WINDOW{1}; + const std::chrono::minutes CREDENTIALS_SHORT_LIFETIME{20}; + const std::chrono::minutes CREDENTIALS_MEDIUM_LIFETIME{90}; + const std::chrono::minutes CREDENTIALS_SHORT_ADVISORY_WINDOW{5}; + const std::chrono::minutes CREDENTIALS_MEDIUM_ADVISORY_WINDOW{15}; + const std::chrono::minutes CREDENTIALS_LONG_ADVISORY_WINDOW{60}; + const std::chrono::minutes CREDENTIALS_REFRESH_BACKOFF_MIN{5}; + const std::chrono::minutes CREDENTIALS_REFRESH_BACKOFF_MAX{10}; + const std::chrono::seconds CREDENTIALS_ERROR_CACHE_MIN{1}; + const std::chrono::seconds CREDENTIALS_ERROR_CACHE_MAX{5}; + + // RefreshResult is the outcome of one credential retrieve, built with Success(), Recoverable(), or NonRecoverable(). + template + class RefreshResult + { + public: + enum class Status + { + Success, + Recoverable, + NonRecoverable + }; + + static RefreshResult Success(CredentialsT credentials, Aws::Utils::DateTime expiration) + { + return RefreshResult(Status::Success, std::move(credentials), + Aws::Crt::Optional(expiration), {}); + } + static RefreshResult Recoverable(Aws::String errorMessage = {}) + { + return RefreshResult(Status::Recoverable, CredentialsT{}, {}, std::move(errorMessage)); + } + static RefreshResult NonRecoverable(Aws::String errorMessage) + { + return RefreshResult(Status::NonRecoverable, CredentialsT{}, {}, std::move(errorMessage)); + } + + Status status; + CredentialsT credentials; + Aws::Crt::Optional expiration; + Aws::String errorMessage; + + private: + RefreshResult(Status status, CredentialsT credentials, Aws::Crt::Optional expiration, + Aws::String errorMessage) + : status(status), credentials(std::move(credentials)), + expiration(std::move(expiration)), errorMessage(std::move(errorMessage)) {} + }; + + // RefreshFailureStrategy decides how long cached credentials are served after a failed refresh. + class AWS_CORE_LOCAL RefreshFailureStrategy + { + public: + // BackoffRange bounds the jittered wait before the next refresh attempt. + struct BackoffRange + { + std::chrono::milliseconds min; + std::chrono::milliseconds max; + }; + + virtual ~RefreshFailureStrategy() = default; + + // Backoff returns the range to back off within while serving cached credentials, or none to serve them only until they expire. + virtual Aws::Crt::Optional Backoff() const = 0; + }; + + // StaticStabilityStrategy keeps serving cached credentials, even expired, and backs off before the next refresh attempt. + class AWS_CORE_LOCAL StaticStabilityStrategy : public RefreshFailureStrategy + { + public: + Aws::Crt::Optional Backoff() const override + { + return BackoffRange{CREDENTIALS_REFRESH_BACKOFF_MIN, CREDENTIALS_REFRESH_BACKOFF_MAX}; + } + }; + + // CredentialsCachingState caches credentials and runs the refresh lifecycle for CredentialsCachingProvider. + template + class AWS_CORE_LOCAL CredentialsCachingState + { + public: + using RetrieveFunction = std::function()>; + using ClockFunction = std::function; + using JitterFunction = std::function; + using IdentifierFunction = std::function; + + explicit CredentialsCachingState(RetrieveFunction retrieve, + IdentifierFunction identifier, + std::shared_ptr failureStrategy, + ClockFunction clock = &Aws::Utils::DateTime::Now, + JitterFunction jitter = &CredentialsCachingState::DefaultJitter) + : m_retrieve(std::move(retrieve)), m_identifier(std::move(identifier)), m_failureStrategy(std::move(failureStrategy)), + m_clock(std::move(clock)), m_jitter(std::move(jitter)) + { + } + + // GetCredentials returns cached credentials, refreshing them first when a refresh is due. + Aws::Utils::Outcome GetCredentials() + { + const int64_t nowMs = m_clock().Millis(); + CredentialsT cachedCredentials{}; + bool advisory = false; + { + Aws::Utils::Threading::ReaderLockGuard guard(m_stateLock); + if (m_entry.credentials) + { + if (!RefreshNeeded(m_entry, nowMs)) + { + return Aws::Utils::Outcome(*m_entry.credentials); + } + if (RateLimited(m_entry, nowMs)) + { + return Aws::Utils::Outcome(*m_entry.credentials); + } + cachedCredentials = *m_entry.credentials; + advisory = !MandatoryRefreshNeeded(m_entry, nowMs); + } + } + + if (advisory) + { + return AdvisoryRefresh(cachedCredentials); + } + return WaitForRetrieve(); + } + + // Invalidate expires the cached credentials if they match rejectedIdentifier, keeping them and the backoff. + void Invalidate(const Aws::String& rejectedIdentifier) + { + Aws::Utils::Threading::WriterLockGuard guard(m_stateLock); + if (!m_entry.credentials) + { + return; + } + if (m_identifier(*m_entry.credentials) != rejectedIdentifier) + { + return; + } + m_entry.expiration = m_clock(); + } + + private: + // CacheEntry holds the cached credentials, their refresh timing, and any cached non-recoverable error. + struct CacheEntry + { + Aws::Crt::Optional credentials; + Aws::Utils::DateTime expiration; + std::chrono::milliseconds advisoryWindow{CREDENTIALS_MEDIUM_ADVISORY_WINDOW}; + int64_t nextRefreshAllowedAtMs{0}; + + Aws::String errorMessage; + int64_t errorExpiresAtMs{0}; + }; + + // AdvisoryRefresh runs SingleRetrieve if no refresh is in flight, otherwise returns cachedCredentials. + Aws::Utils::Outcome AdvisoryRefresh(const CredentialsT& cachedCredentials) + { + std::unique_lock lock(m_refreshLock, std::try_to_lock); + if (!lock.owns_lock()) + { + return Aws::Utils::Outcome(cachedCredentials); + } + return SingleRetrieve(); + } + + // WaitForRetrieve waits for any in-flight refresh, then runs SingleRetrieve; used for the initial and mandatory refresh. + Aws::Utils::Outcome WaitForRetrieve() + { + std::unique_lock lock(m_refreshLock); + return SingleRetrieve(); + } + + // SingleRetrieve asks the source for new credentials and caches the result; must be called with m_refreshLock held. + Aws::Utils::Outcome SingleRetrieve() + { + { + const int64_t nowMs = m_clock().Millis(); + Aws::Utils::Threading::ReaderLockGuard guard(m_stateLock); + if (m_entry.credentials && !RefreshNeeded(m_entry, nowMs)) + { + return Aws::Utils::Outcome(*m_entry.credentials); + } + if (NonRecoverableErrCached(nowMs)) + { + return Aws::Utils::Outcome(m_entry.errorMessage); + } + if (m_entry.credentials && RateLimited(m_entry, nowMs)) + { + return Aws::Utils::Outcome(*m_entry.credentials); + } + } + + const RefreshResult result = m_retrieve(); + const int64_t nowMs = m_clock().Millis(); + + Aws::Utils::Threading::WriterLockGuard guard(m_stateLock); + if (result.status == RefreshResult::Status::Success && + result.expiration.has_value() && result.expiration->Millis() > nowMs) + { + CacheEntry entry; + entry.credentials = result.credentials; + entry.expiration = result.expiration.value(); + entry.advisoryWindow = AdvisoryWindowFor(std::chrono::milliseconds(result.expiration->Millis() - nowMs)); + m_entry = std::move(entry); + return Aws::Utils::Outcome(*m_entry.credentials); + } + + if (result.status == RefreshResult::Status::NonRecoverable) + { + m_entry.errorMessage = result.errorMessage; + m_entry.errorExpiresAtMs = nowMs + Jitter(CREDENTIALS_ERROR_CACHE_MIN, CREDENTIALS_ERROR_CACHE_MAX).count(); + return Aws::Utils::Outcome(result.errorMessage); + } + + const Aws::Crt::Optional backoffRange = m_failureStrategy->Backoff(); + if (m_entry.credentials && backoffRange) + { + const std::chrono::milliseconds backoff = Jitter(backoffRange->min, backoffRange->max); + m_entry.nextRefreshAllowedAtMs = nowMs + backoff.count(); + AWS_LOGSTREAM_WARN("CredentialsCachingState", "Credential refresh failed: " + << (result.errorMessage.empty() ? Aws::String("the credential source returned expired credentials") : result.errorMessage) + << ". The SDK will continue using cached credentials. A refresh of these credentials will be attempted again after " + << std::chrono::duration_cast(backoff).count() << " seconds."); + return Aws::Utils::Outcome(*m_entry.credentials); + } + if (m_entry.credentials && nowMs < m_entry.expiration.Millis()) + { + return Aws::Utils::Outcome(*m_entry.credentials); + } + return Aws::Utils::Outcome( + result.errorMessage.empty() ? Aws::String("NoCredentialsError") : result.errorMessage); + } + + // RefreshNeeded returns whether cached is inside its advisory window. + static bool RefreshNeeded(const CacheEntry& entry, int64_t nowMs) + { + return nowMs >= entry.expiration.Millis() - entry.advisoryWindow.count(); + } + + // MandatoryRefreshNeeded returns whether cached is inside its mandatory window. + static bool MandatoryRefreshNeeded(const CacheEntry& entry, int64_t nowMs) + { + return nowMs >= entry.expiration.Millis() - std::chrono::milliseconds(CREDENTIALS_MANDATORY_REFRESH_WINDOW).count(); + } + + // RateLimited returns whether a failed refresh's backoff is still active for entry. + static bool RateLimited(const CacheEntry& entry, int64_t nowMs) + { + return entry.nextRefreshAllowedAtMs != 0 && nowMs < entry.nextRefreshAllowedAtMs; + } + + // NonRecoverableErrCached returns whether a non-recoverable error is cached and not yet expired. + bool NonRecoverableErrCached(int64_t nowMs) const + { + return nowMs < m_entry.errorExpiresAtMs; + } + + // AdvisoryWindowFor returns the advisory window for a credential lifetime. + static std::chrono::milliseconds AdvisoryWindowFor(std::chrono::milliseconds lifetime) + { + if (lifetime <= CREDENTIALS_SHORT_LIFETIME) + { + return CREDENTIALS_SHORT_ADVISORY_WINDOW; + } + if (lifetime < CREDENTIALS_MEDIUM_LIFETIME) + { + return CREDENTIALS_MEDIUM_ADVISORY_WINDOW; + } + return CREDENTIALS_LONG_ADVISORY_WINDOW; + } + + // Jitter returns a random duration in [lo, hi]. + std::chrono::milliseconds Jitter(std::chrono::milliseconds lo, std::chrono::milliseconds hi) const + { + return lo + std::chrono::milliseconds(static_cast(m_jitter() * static_cast((hi - lo).count() + 1))); + } + + static double DefaultJitter() + { + static thread_local std::mt19937 generator{std::random_device{}()}; + std::uniform_real_distribution distribution(0.0, 1.0); + return distribution(generator); + } + + RetrieveFunction m_retrieve; + IdentifierFunction m_identifier; + std::shared_ptr m_failureStrategy; + ClockFunction m_clock; + JitterFunction m_jitter; + + mutable Aws::Utils::Threading::ReaderWriterLock m_stateLock; + std::mutex m_refreshLock; + CacheEntry m_entry; + }; + } // namespace Internal +} // namespace Aws diff --git a/src/aws-cpp-sdk-core/include/aws/core/internal/CredentialsCachingProvider.h b/src/aws-cpp-sdk-core/include/aws/core/internal/CredentialsCachingProvider.h new file mode 100644 index 000000000000..12e0692dff85 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/aws/core/internal/CredentialsCachingProvider.h @@ -0,0 +1,61 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Aws +{ + namespace Internal + { + class CredentialsCachingStateImpl; + + // IsCredentialsRejectedError returns whether a service rejected the credentials that signed the request. + inline bool IsCredentialsRejectedError(const Aws::Client::AWSError& error) + { + const Aws::String& errorCode = error.GetExceptionName(); + return errorCode == "ExpiredToken" || errorCode == "InvalidToken"; + } + + /** + * RefreshFailurePolicy picks what CredentialsCachingProvider serves after a failed refresh. + */ + enum class RefreshFailurePolicy + { + StaticStability, + ServeUntilExpiry + }; + + /** + * CredentialsCachingProvider caches another provider's credentials and refreshes them per the refresh lifecycle. + */ + class AWS_CORE_API CredentialsCachingProvider final : public Aws::Auth::AWSCredentialsProvider + { + public: + using RetrieveFunction = std::function; + + explicit CredentialsCachingProvider(RetrieveFunction retrieve, + RefreshFailurePolicy failurePolicy = RefreshFailurePolicy::StaticStability); + + Aws::Auth::AWSCredentials GetAWSCredentials() override; + + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + + private: + RetrieveFunction m_retrieve; + std::shared_ptr m_cachingState; + }; + } // namespace Internal +} // namespace Aws diff --git a/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClient.h b/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClient.h index c9909d40dd18..535dbe12eb62 100644 --- a/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClient.h +++ b/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClient.h @@ -232,6 +232,10 @@ namespace client return AwsClientRequestSigning::SignEventMessage(message, seed, ctx, m_authSchemes); } + void InvalidateIdentity(const AwsSmithyClientAsyncRequestContext& ctx, const Aws::String& rejectedIdentifier) const override { + AwsClientRequestSigning::InvalidateIdentity(ctx, m_authSchemes, rejectedIdentifier); + } + IdentityOutcome ResolveIdentity(const AwsSmithyClientAsyncRequestContext& ctx) const override { return AwsClientRequestSigning::ResolveIdentity(ctx, m_authSchemes); } diff --git a/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClientBase.h b/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClientBase.h index 038673b389d5..9f4298fff6cb 100644 --- a/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClientBase.h +++ b/src/aws-cpp-sdk-core/include/smithy/client/AwsSmithyClientBase.h @@ -231,6 +231,8 @@ namespace client bool AdjustClockSkew(HttpResponseOutcome& outcome, const AwsSmithyClientAsyncRequestContext& ctx) const; void RecordClockSkew(const Aws::Http::HttpResponse& response, const AwsSmithyClientAsyncRequestContext& ctx) const; virtual IdentityOutcome ResolveIdentity(const AwsSmithyClientAsyncRequestContext& ctx) const = 0; + virtual void InvalidateIdentity(const AwsSmithyClientAsyncRequestContext& ctx, const Aws::String& rejectedIdentifier) const = 0; + void InvalidateCredentials(const AWSCoreError& error, const AwsSmithyClientAsyncRequestContext& ctx) const; virtual GetContextEndpointParametersOutcome GetContextEndpointParameters(const AwsSmithyClientAsyncRequestContext& ctx) const = 0; AwsSmithyClientBase::ResolveEndpointOutcome ResolveEndpointFromRequest( Aws::AmazonWebServiceRequest const * const request, diff --git a/src/aws-cpp-sdk-core/include/smithy/client/common/AwsSmithyRequestSigning.h b/src/aws-cpp-sdk-core/include/smithy/client/common/AwsSmithyRequestSigning.h index 3ed803c40658..36d096ae1a2f 100644 --- a/src/aws-cpp-sdk-core/include/smithy/client/common/AwsSmithyRequestSigning.h +++ b/src/aws-cpp-sdk-core/include/smithy/client/common/AwsSmithyRequestSigning.h @@ -64,6 +64,21 @@ namespace smithy return std::move(*visitor.result); } + static void InvalidateIdentity(const client::AwsSmithyClientAsyncRequestContext& ctx, + const Aws::UnorderedMap& authSchemes, + const Aws::String& rejectedIdentifier) + { + auto authSchemeOutcome = ResolveAuthScheme(ctx.m_authSchemeOption, authSchemes); + if (!authSchemeOutcome.IsSuccess()) + { + AWS_LOGSTREAM_DEBUG(AWS_SMITHY_CLIENT_SIGNING_TAG, "Not invalidating identity: no auth scheme for " << ctx.m_authSchemeOption.schemeId); + return; + } + InvalidateVisitor visitor(rejectedIdentifier); + AuthSchemesVariantT authSchemesVariantCopy(authSchemeOutcome.GetResult().value()); + authSchemesVariantCopy.Visit(visitor); + } + static SigningOutcome SignRequest(std::shared_ptr HTTPRequest, const client::AwsSmithyClientAsyncRequestContext& ctx, const Aws::UnorderedMap& authSchemes) @@ -187,6 +202,27 @@ namespace smithy } }; + struct InvalidateVisitor + { + explicit InvalidateVisitor(const Aws::String& rejectedIdentifier): m_rejectedIdentifier(rejectedIdentifier) + { + } + + const Aws::String& m_rejectedIdentifier; + + template + void operator()(AuthSchemeAlternativeT& authScheme) + { + auto identityResolver = authScheme.identityResolver(); + if (!identityResolver) + { + AWS_LOGSTREAM_DEBUG(AWS_SMITHY_CLIENT_SIGNING_TAG, "Not invalidating identity: auth scheme has no identity resolver"); + return; + } + identityResolver->invalidate(m_rejectedIdentifier); + } + }; + struct SignerVisitor { SignerVisitor(std::shared_ptr httpRequest, const client::AwsSmithyClientAsyncRequestContext& ctx) diff --git a/src/aws-cpp-sdk-core/include/smithy/identity/resolver/AwsIdentityResolverBase.h b/src/aws-cpp-sdk-core/include/smithy/identity/resolver/AwsIdentityResolverBase.h index 5f89e7754114..4a62fafa4ad0 100644 --- a/src/aws-cpp-sdk-core/include/smithy/identity/resolver/AwsIdentityResolverBase.h +++ b/src/aws-cpp-sdk-core/include/smithy/identity/resolver/AwsIdentityResolverBase.h @@ -32,5 +32,8 @@ namespace smithy { // Identity. An identity resolver might load the identity from a remote service (e.g. STS), a local // service (e.g. IMDS), local disk (e.g. a configuration file) or local memory (e.g. environment variables). virtual ResolveIdentityFutureOutcome getIdentity(const IdentityProperties& identityProperties, const AdditionalParameters& additionalParameters) = 0; + + // invalidate tells a caching resolver that a service rejected the identity with this identifier. + virtual void invalidate(const Aws::String& /*rejectedIdentifier*/) {} }; } \ No newline at end of file diff --git a/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/AwsCredentialsProviderIdentityResolver.h b/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/AwsCredentialsProviderIdentityResolver.h index dc72fd068f33..57cfa6eb1a3c 100644 --- a/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/AwsCredentialsProviderIdentityResolver.h +++ b/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/AwsCredentialsProviderIdentityResolver.h @@ -45,6 +45,11 @@ namespace smithy return {std::move(smithyCreds)}; } + void invalidate(const Aws::String& rejectedIdentifier) override + { + m_credentialsProvider->Invalidate(rejectedIdentifier); + } + protected: std::shared_ptr m_credentialsProvider; }; diff --git a/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/DefaultAwsCredentialIdentityResolver.h b/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/DefaultAwsCredentialIdentityResolver.h index 560a2960ae44..8027ff393076 100644 --- a/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/DefaultAwsCredentialIdentityResolver.h +++ b/src/aws-cpp-sdk-core/include/smithy/identity/resolver/built-in/DefaultAwsCredentialIdentityResolver.h @@ -52,5 +52,7 @@ class DefaultAwsCredentialIdentityResolver : public AwsCredentialIdentityResolve return ResolveIdentityFutureOutcome(std::move(smithyCreds)); } + + void invalidate(const Aws::String& rejectedIdentifier) override { legacyChain_sp->Invalidate(rejectedIdentifier); } }; } // namespace smithy diff --git a/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProvider.cpp b/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProvider.cpp index e009b64c98df..19bd0903013c 100644 --- a/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProvider.cpp @@ -5,6 +5,7 @@ #include +#include #include #include @@ -238,43 +239,34 @@ void ProfileConfigFileAWSCredentialsProvider::RefreshIfExpired() static const char* INSTANCE_LOG_TAG = "InstanceProfileCredentialsProvider"; InstanceProfileCredentialsProvider::InstanceProfileCredentialsProvider(long refreshRateMs) : - m_ec2MetadataConfigLoader(Aws::MakeShared(INSTANCE_LOG_TAG)), - m_loadFrequencyMs(refreshRateMs) + m_ec2MetadataConfigLoader(Aws::MakeShared(INSTANCE_LOG_TAG)) { AWS_LOGSTREAM_INFO(INSTANCE_LOG_TAG, "Creating Instance with default EC2MetadataClient and refresh rate " << refreshRateMs); } InstanceProfileCredentialsProvider::InstanceProfileCredentialsProvider(const std::shared_ptr& loader, long refreshRateMs) : - m_ec2MetadataConfigLoader(loader), - m_loadFrequencyMs(refreshRateMs) + m_ec2MetadataConfigLoader(loader) { AWS_LOGSTREAM_INFO(INSTANCE_LOG_TAG, "Creating Instance with injected EC2MetadataClient and refresh rate " << refreshRateMs); } InstanceProfileCredentialsProvider::InstanceProfileCredentialsProvider(const Aws::Client::ClientConfiguration::CredentialProviderConfiguration& credentialConfig, long refreshRateMs) : - m_ec2MetadataConfigLoader(Aws::MakeShared(INSTANCE_LOG_TAG, credentialConfig)), - m_loadFrequencyMs(refreshRateMs) + m_ec2MetadataConfigLoader(Aws::MakeShared(INSTANCE_LOG_TAG, credentialConfig)) { + AWS_UNREFERENCED_PARAM(refreshRateMs); AWS_LOGSTREAM_INFO(INSTANCE_LOG_TAG, "Creating Instance with IMDS timeout: " << credentialConfig.imdsConfig.metadataServiceTimeout << "s, attempts: " << credentialConfig.imdsConfig.metadataServiceNumAttempts); } AWSCredentials InstanceProfileCredentialsProvider::GetAWSCredentials() { - RefreshIfExpired(); - ReaderLockGuard guard(m_reloadLock); if (m_ec2MetadataConfigLoader) { - const Aws::Map &profiles = m_ec2MetadataConfigLoader->GetProfiles(); - auto profileIter = profiles.find(Aws::Config::INSTANCE_PROFILE_KEY); - - if (profileIter != profiles.end()) { - AWSCredentials credentials = profileIter->second.GetCredentials(); - if (!credentials.IsEmpty()) { - credentials.AddUserAgentFeature(UserAgentFeature::CREDENTIALS_IMDS); - } - return credentials; + AWSCredentials credentials = m_ec2MetadataConfigLoader->GetCredentials(); + if (!credentials.IsEmpty()) { + credentials.AddUserAgentFeature(UserAgentFeature::CREDENTIALS_IMDS); } + return credentials; } else { @@ -284,54 +276,21 @@ AWSCredentials InstanceProfileCredentialsProvider::GetAWSCredentials() return AWSCredentials(); } -bool InstanceProfileCredentialsProvider::ExpiresSoon() const +void InstanceProfileCredentialsProvider::Invalidate(const Aws::String& accessKeyId) { - auto profileIter = m_ec2MetadataConfigLoader->GetProfiles().find(Aws::Config::INSTANCE_PROFILE_KEY); - AWSCredentials credentials; - - if(profileIter != m_ec2MetadataConfigLoader->GetProfiles().end()) + if (m_ec2MetadataConfigLoader) { - credentials = profileIter->second.GetCredentials(); + m_ec2MetadataConfigLoader->Invalidate(accessKeyId); } - - return credentials.ExpiresSoon(AWS_CREDENTIAL_PROVIDER_EXPIRATION_GRACE_PERIOD); } void InstanceProfileCredentialsProvider::Reload() { - AWS_LOGSTREAM_INFO(INSTANCE_LOG_TAG, "Credentials have expired attempting to re-pull from EC2 Metadata Service."); - if (m_ec2MetadataConfigLoader) { - m_ec2MetadataConfigLoader->Load(); - AWSCredentialsProvider::Reload(); - } else { - AWS_LOGSTREAM_ERROR(INSTANCE_LOG_TAG, "EC2 Metadata config loader is a nullptr"); - } -} - -void InstanceProfileCredentialsProvider::RefreshIfExpired() -{ - AWS_LOGSTREAM_DEBUG(INSTANCE_LOG_TAG, "Checking if latest credential pull has expired."); - ReaderLockGuard guard(m_reloadLock); - auto profileIter = m_ec2MetadataConfigLoader->GetProfiles().find(Aws::Config::INSTANCE_PROFILE_KEY); - AWSCredentials credentials; - - if(profileIter != m_ec2MetadataConfigLoader->GetProfiles().end()) + if (m_ec2MetadataConfigLoader) { - credentials = profileIter->second.GetCredentials(); - - if (!credentials.IsEmpty() && !IsTimeToRefresh(m_loadFrequencyMs) && !ExpiresSoon()) - { - return; - } - - guard.UpgradeToWriterLock(); - if (!credentials.IsEmpty() && !IsTimeToRefresh(m_loadFrequencyMs) && !ExpiresSoon()) // double-checked lock to avoid refreshing twice - { - return; - } + m_ec2MetadataConfigLoader->GetCredentials(); } - - Reload(); + AWSCredentialsProvider::Reload(); } static const char PROCESS_LOG_TAG[] = "ProcessCredentialsProvider"; diff --git a/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProviderChain.cpp b/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProviderChain.cpp index d2ea774faf8b..04ffb3953c11 100644 --- a/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProviderChain.cpp +++ b/src/aws-cpp-sdk-core/source/auth/AWSCredentialsProviderChain.cpp @@ -43,6 +43,15 @@ AWSCredentials AWSCredentialsProviderChain::GetAWSCredentials() return AWSCredentials(); } +void AWSCredentialsProviderChain::Invalidate(const Aws::String& accessKeyId) +{ + ReaderLockGuard lock(m_cachedProviderLock); + if (m_cachedProvider) + { + m_cachedProvider->Invalidate(accessKeyId); + } +} + DefaultAWSCredentialsProviderChain::DefaultAWSCredentialsProviderChain() : AWSCredentialsProviderChain() { AddProvider(Aws::MakeShared(DefaultCredentialsProviderChainTag)); diff --git a/src/aws-cpp-sdk-core/source/auth/CredentialsCachingProvider.cpp b/src/aws-cpp-sdk-core/source/auth/CredentialsCachingProvider.cpp new file mode 100644 index 000000000000..55d727dcd6d8 --- /dev/null +++ b/src/aws-cpp-sdk-core/source/auth/CredentialsCachingProvider.cpp @@ -0,0 +1,86 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include + +using namespace Aws::Auth; +using namespace Aws::Internal; +using namespace Aws::Utils; + +namespace Aws +{ + namespace Internal + { + class AWS_CORE_LOCAL CredentialsCachingStateImpl : public CredentialsCachingState + { + public: + using CredentialsCachingState::CredentialsCachingState; + }; + } +} + +namespace +{ + // ServeUntilExpiryStrategy serves cached credentials after a failed refresh only until they expire. + class ServeUntilExpiryStrategy : public RefreshFailureStrategy + { + public: + Aws::Crt::Optional Backoff() const override + { + return Aws::Crt::Optional(); + } + }; + + std::shared_ptr MakeFailureStrategy(const char* tag, RefreshFailurePolicy failurePolicy) + { + if (failurePolicy == RefreshFailurePolicy::ServeUntilExpiry) + { + return Aws::MakeShared(tag); + } + return Aws::MakeShared(tag); + } +} + +static const char CREDENTIALS_CACHING_PROVIDER_LOG_TAG[] = "CredentialsCachingProvider"; + +CredentialsCachingProvider::CredentialsCachingProvider(RetrieveFunction retrieve, RefreshFailurePolicy failurePolicy) + : m_retrieve(std::move(retrieve)), + m_cachingState(Aws::MakeShared(CREDENTIALS_CACHING_PROVIDER_LOG_TAG, + [this]() -> Aws::Internal::RefreshResult { + AWSCredentials credentials = m_retrieve(); + if (credentials.IsEmpty()) + { + return Aws::Internal::RefreshResult::Recoverable("credential source returned no credentials"); + } + Aws::Utils::DateTime expiration = credentials.GetExpiration(); + if (!expiration.WasParseSuccessful()) + { + expiration = Aws::Utils::DateTime((std::chrono::time_point::max)()); + } + return Aws::Internal::RefreshResult::Success(credentials, expiration); + }, + [](const AWSCredentials& credentials) { return credentials.GetAWSAccessKeyId(); }, + MakeFailureStrategy(CREDENTIALS_CACHING_PROVIDER_LOG_TAG, failurePolicy))) +{ +} + +AWSCredentials CredentialsCachingProvider::GetAWSCredentials() +{ + auto outcome = m_cachingState->GetCredentials(); + if (!outcome.IsSuccess()) + { + AWS_LOGSTREAM_ERROR(CREDENTIALS_CACHING_PROVIDER_LOG_TAG, "Credential retrieve failed and no cached credentials are available, returning empty: " << outcome.GetError()); + return AWSCredentials(); + } + return outcome.GetResult(); +} + +void CredentialsCachingProvider::Invalidate(const Aws::String& accessKeyId) +{ + m_cachingState->Invalidate(accessKeyId); +} diff --git a/src/aws-cpp-sdk-core/source/auth/CrtCredentialsProvider.cpp b/src/aws-cpp-sdk-core/source/auth/CrtCredentialsProvider.cpp index 5bdbe83f3e59..1eec902f54d6 100644 --- a/src/aws-cpp-sdk-core/source/auth/CrtCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-core/source/auth/CrtCredentialsProvider.cpp @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ #include +#include #include #include #include @@ -16,7 +17,6 @@ using namespace Aws::Utils; using namespace Aws::Utils::Threading; namespace { -const int FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000; const char* CRT_CREDS_PROVIDER_TAG = "CrtCredentialsProvider"; struct RefreshState { @@ -27,13 +27,27 @@ struct RefreshState { }; } // namespace +AWSCredentials CrtCredentialsProvider::RetrieveForCache() { + m_credentials = AWSCredentials{}; + Reload(); + return m_credentials; +} + CrtCredentialsProvider::CrtCredentialsProvider( const std::function()>& credentialsProviderFactory, std::chrono::milliseconds providerFuturesTimeoutMs, Aws::Client::UserAgentFeature userAgentFeature, const Aws::String& providerName) + : CrtCredentialsProvider(credentialsProviderFactory, providerFuturesTimeoutMs, userAgentFeature, providerName, + Aws::Internal::RefreshFailurePolicy::StaticStability) {} + +CrtCredentialsProvider::CrtCredentialsProvider( + const std::function()>& credentialsProviderFactory, + std::chrono::milliseconds providerFuturesTimeoutMs, Aws::Client::UserAgentFeature userAgentFeature, const Aws::String& providerName, + Aws::Internal::RefreshFailurePolicy failurePolicy) : m_credentialsProvider{credentialsProviderFactory()}, m_providerFuturesTimeoutMs{providerFuturesTimeoutMs}, m_userAgentFeature{userAgentFeature}, - m_providerName{providerName} { + m_providerName{providerName}, + m_cachingProvider{Aws::MakeShared(CRT_CREDS_PROVIDER_TAG, [this]() { return RetrieveForCache(); }, failurePolicy)} { if (m_credentialsProvider && m_credentialsProvider->IsValid()) { m_state = STATE::INITIALIZED; } @@ -45,9 +59,14 @@ AWSCredentials CrtCredentialsProvider::GetAWSCredentials() { if (m_state != STATE::INITIALIZED) { return AWSCredentials{}; } - RefreshIfExpired(); - const ReaderLockGuard guard(m_reloadLock); - return m_credentials; + return m_cachingProvider->GetAWSCredentials(); +} + +void CrtCredentialsProvider::Invalidate(const Aws::String& accessKeyId) { + if (m_state != STATE::INITIALIZED) { + return; + } + m_cachingProvider->Invalidate(accessKeyId); } void CrtCredentialsProvider::Reload() { @@ -78,21 +97,6 @@ void CrtCredentialsProvider::Reload() { m_credentials = std::move(credentials); } -void CrtCredentialsProvider::RefreshIfExpired() { - ReaderLockGuard guard(m_reloadLock); - if (!m_credentials.IsEmpty() && !m_credentials.ExpiresSoon(FIVE_MINUTES_IN_MILLIS)) { - return; - } - - guard.UpgradeToWriterLock(); - // double-checked lock to avoid refreshing twice - if (!m_credentials.IsEmpty() && !m_credentials.ExpiresSoon(FIVE_MINUTES_IN_MILLIS)) { - return; - } - - Reload(); -} - AWSCredentials CrtCredentialsProvider::ExtractCredentialsFromCrt(const Aws::Crt::Auth::Credentials& crtCredentials) { AWSCredentials credentials{}; const auto accountIdCursor = crtCredentials.GetAccessKeyId(); diff --git a/src/aws-cpp-sdk-core/source/auth/GeneralHTTPCredentialsProvider.cpp b/src/aws-cpp-sdk-core/source/auth/GeneralHTTPCredentialsProvider.cpp index 416e50dd648e..425ca108c924 100644 --- a/src/aws-cpp-sdk-core/source/auth/GeneralHTTPCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-core/source/auth/GeneralHTTPCredentialsProvider.cpp @@ -5,6 +5,7 @@ #include +#include #include #include @@ -16,6 +17,13 @@ using namespace Aws::Auth; static const char GEN_HTTP_LOG_TAG[] = "GeneralHTTPCredentialsProvider"; +AWSCredentials GeneralHTTPCredentialsProvider::RetrieveForCache() +{ + m_credentials = AWSCredentials(); + Reload(); + return m_credentials; +} + const char GeneralHTTPCredentialsProvider::AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE[] = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"; const char GeneralHTTPCredentialsProvider::AWS_CONTAINER_CREDENTIALS_RELATIVE_URI[] = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; const char GeneralHTTPCredentialsProvider::AWS_CONTAINER_CREDENTIALS_FULL_URI[] = "AWS_CONTAINER_CREDENTIALS_FULL_URI"; @@ -154,7 +162,7 @@ GeneralHTTPCredentialsProvider::GeneralHTTPCredentialsProvider(const Aws::String long refreshRateMs, ShouldCreateFunc shouldCreateFunc) : m_authTokenFilePath(authTokenFilePath), - m_loadFrequencyMs(refreshRateMs) + m_cachingProvider(Aws::MakeShared(GEN_HTTP_LOG_TAG, [this]() { return RetrieveForCache(); })) { if (shouldCreateFunc(relativeUri, absoluteUri, authToken)) { @@ -170,21 +178,19 @@ GeneralHTTPCredentialsProvider::GeneralHTTPCredentialsProvider(const Aws::String GeneralHTTPCredentialsProvider::GeneralHTTPCredentialsProvider( const std::shared_ptr& client, long refreshRateMs) : m_ecsCredentialsClient(client), - m_loadFrequencyMs(refreshRateMs) + m_cachingProvider(Aws::MakeShared(GEN_HTTP_LOG_TAG, [this]() { return RetrieveForCache(); })) { AWS_LOGSTREAM_INFO(GEN_HTTP_LOG_TAG, "Creating GeneralHTTPCredentialsProvider with a pre-allocated client " << refreshRateMs); } AWSCredentials GeneralHTTPCredentialsProvider::GetAWSCredentials() { - RefreshIfExpired(); - Aws::Utils::Threading::ReaderLockGuard guard(m_reloadLock); - return m_credentials; + return m_cachingProvider->GetAWSCredentials(); } -bool GeneralHTTPCredentialsProvider::ExpiresSoon() const +void GeneralHTTPCredentialsProvider::Invalidate(const Aws::String& accessKeyId) { - return ((m_credentials.GetExpiration() - Aws::Utils::DateTime::Now()).count() < AWS_CREDENTIAL_PROVIDER_EXPIRATION_GRACE_PERIOD); + m_cachingProvider->Invalidate(accessKeyId); } Aws::String GeneralHTTPCredentialsProvider::LoadTokenFromFile() const @@ -250,22 +256,3 @@ void GeneralHTTPCredentialsProvider::Reload() } AWSCredentialsProvider::Reload(); } - -void GeneralHTTPCredentialsProvider::RefreshIfExpired() -{ - AWS_LOGSTREAM_DEBUG(GEN_HTTP_LOG_TAG, "Checking if latest credential pull has expired."); - Aws::Utils::Threading::ReaderLockGuard guard(m_reloadLock); - if (!m_credentials.IsEmpty() && !IsTimeToRefresh(m_loadFrequencyMs) && !ExpiresSoon()) - { - return; - } - - guard.UpgradeToWriterLock(); - - if (!m_credentials.IsEmpty() && !IsTimeToRefresh(m_loadFrequencyMs) && !ExpiresSoon()) - { - return; - } - - Reload(); -} diff --git a/src/aws-cpp-sdk-core/source/auth/ProfileCredentialsProvider.cpp b/src/aws-cpp-sdk-core/source/auth/ProfileCredentialsProvider.cpp index b37ead67edda..768c07f07372 100644 --- a/src/aws-cpp-sdk-core/source/auth/ProfileCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-core/source/auth/ProfileCredentialsProvider.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -46,6 +48,23 @@ std::shared_ptr GetProfileCrtProvider( return CredentialsProvider::CreateCredentialsProviderProfile(config); } + +// UsesCredentialProcess returns whether the CRT profile provider resolves profileName through credential_process. +bool UsesCredentialProcess(const Aws::String& profileName) { + const Aws::Config::Profile config = Aws::Config::GetCachedConfigProfile(profileName); + const Aws::Config::Profile credentials = Aws::Config::GetCachedCredentialsProfile(profileName); + const bool hasProcess = !config.GetCredentialProcess().empty() || !credentials.GetCredentialProcess().empty(); + const bool hasRoleArn = !config.GetRoleArn().empty() || !credentials.GetRoleArn().empty(); + const bool hasStaticKeys = !config.GetCredentials().GetAWSAccessKeyId().empty() || !config.GetCredentials().GetAWSSecretKey().empty() || + !credentials.GetCredentials().GetAWSAccessKeyId().empty() || !credentials.GetCredentials().GetAWSSecretKey().empty(); + return hasProcess && !hasRoleArn && !hasStaticKeys; +} + +// FailurePolicyFor serves credential_process credentials only until they expire. +Aws::Internal::RefreshFailurePolicy FailurePolicyFor(const Aws::String& profileName) { + return UsesCredentialProcess(profileName) ? Aws::Internal::RefreshFailurePolicy::ServeUntilExpiry + : Aws::Internal::RefreshFailurePolicy::StaticStability; +} } // namespace class ProfileCredentialsProvider::ProfileCredentialsProviderImp : public CrtCredentialsProvider { @@ -56,7 +75,8 @@ class ProfileCredentialsProvider::ProfileCredentialsProviderImp : public CrtCred CredentialsProviderProfileConfig config; return CredentialsProvider::CreateCredentialsProviderProfile(config); }, - std::chrono::milliseconds(DEFAULT_REFRESH_RATE_MS), UserAgentFeature::CREDENTIALS_PROFILE, "ProfileCredentialsProvider") {} + std::chrono::milliseconds(DEFAULT_REFRESH_RATE_MS), UserAgentFeature::CREDENTIALS_PROFILE, "ProfileCredentialsProvider", + FailurePolicyFor(GetConfigProfileName())) {} ProfileCredentialsProviderImp(const Aws::Client::ClientConfiguration::CredentialProviderConfiguration& credentialsConfig) : CrtCredentialsProvider( @@ -64,7 +84,8 @@ class ProfileCredentialsProvider::ProfileCredentialsProviderImp : public CrtCred return GetProfileCrtProvider(credentialsConfig); }, std::chrono::milliseconds(DEFAULT_REFRESH_RATE_MS), UserAgentFeature::CREDENTIALS_PROFILE, - "ProfileCredentialsProvider") {} + "ProfileCredentialsProvider", + FailurePolicyFor(credentialsConfig.profile.empty() ? GetConfigProfileName() : credentialsConfig.profile)) {} static Aws::String GetCredentialsProfileFilename() { auto credentialsFileNameFromVar = Aws::Environment::GetEnv(PROFILE_AWS_CREDENTIALS_FILE); @@ -112,3 +133,5 @@ Aws::String ProfileCredentialsProvider::GetCredentialsProfileFilename() { Aws::String ProfileCredentialsProvider::GetProfileDirectory() { return ProfileCredentialsProviderImp::GetProfileDirectory(); } AWSCredentials ProfileCredentialsProvider::GetAWSCredentials() { return m_impl->GetAWSCredentials(); } + +void ProfileCredentialsProvider::Invalidate(const Aws::String& accessKeyId) { m_impl->Invalidate(accessKeyId); } diff --git a/src/aws-cpp-sdk-core/source/auth/SSOCredentialsProvider.cpp b/src/aws-cpp-sdk-core/source/auth/SSOCredentialsProvider.cpp index 342d1598e12f..57f8eb48f626 100644 --- a/src/aws-cpp-sdk-core/source/auth/SSOCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-core/source/auth/SSOCredentialsProvider.cpp @@ -5,6 +5,7 @@ #include +#include #include #include #include @@ -29,6 +30,13 @@ using Aws::Utils::Threading::ReaderLockGuard; static const char SSO_CREDENTIALS_PROVIDER_LOG_TAG[] = "SSOCredentialsProvider"; +AWSCredentials SSOCredentialsProvider::RetrieveForCache() +{ + m_credentials = AWSCredentials(); + Reload(); + return m_credentials; +} + SSOCredentialsProvider::SSOCredentialsProvider() : SSOCredentialsProvider(GetConfigProfileName(), nullptr) { } @@ -40,7 +48,8 @@ SSOCredentialsProvider::SSOCredentialsProvider(const Aws::String& profile) : SSO SSOCredentialsProvider::SSOCredentialsProvider(const Aws::String& profile, std::shared_ptr config) : m_profileToUse(profile), m_bearerTokenProvider(profile), - m_config(std::move(config)) + m_config(std::move(config)), + m_cachingProvider(Aws::MakeShared(SSO_CREDENTIALS_PROVIDER_LOG_TAG, [this]() { return RetrieveForCache(); })) { AWS_LOGSTREAM_INFO(SSO_CREDENTIALS_PROVIDER_LOG_TAG, "Setting sso credentials provider to read config from " << m_profileToUse); if (!m_config) @@ -56,9 +65,12 @@ SSOCredentialsProvider::SSOCredentialsProvider(const Aws::String& profile, std:: AWSCredentials SSOCredentialsProvider::GetAWSCredentials() { - RefreshIfExpired(); - ReaderLockGuard guard(m_reloadLock); - return m_credentials; + return m_cachingProvider->GetAWSCredentials(); +} + +void SSOCredentialsProvider::Invalidate(const Aws::String& accessKeyId) +{ + m_cachingProvider->Invalidate(accessKeyId); } void SSOCredentialsProvider::Reload() @@ -111,23 +123,6 @@ void SSOCredentialsProvider::Reload() } } -void SSOCredentialsProvider::RefreshIfExpired() -{ - ReaderLockGuard guard(m_reloadLock); - if (!m_credentials.IsExpiredOrEmpty()) - { - return; - } - - guard.UpgradeToWriterLock(); - if (!m_credentials.IsExpiredOrEmpty()) // double-checked lock to avoid refreshing twice - { - return; - } - - Reload(); -} - Aws::String SSOCredentialsProvider::LoadAccessTokenFile(const Aws::String& ssoAccessTokenPath) { AWS_LOGSTREAM_DEBUG(SSO_CREDENTIALS_PROVIDER_LOG_TAG, "Preparing to load token from: " << ssoAccessTokenPath); diff --git a/src/aws-cpp-sdk-core/source/auth/signer/AWSAuthV4Signer.cpp b/src/aws-cpp-sdk-core/source/auth/signer/AWSAuthV4Signer.cpp index 841b12822ff9..fc44012ecd8e 100644 --- a/src/aws-cpp-sdk-core/source/auth/signer/AWSAuthV4Signer.cpp +++ b/src/aws-cpp-sdk-core/source/auth/signer/AWSAuthV4Signer.cpp @@ -85,6 +85,7 @@ bool AWSAuthV4Signer::SignRequestWithSigV4a(Aws::Http::HttpRequest& request, con AWSCredentials credentials = GetCredentials(request.GetServiceSpecificParameters()); UpdateUserAgentWithCredentialFeatures(request, credentials.GetContext()); + request.SetSigningAccessKey(credentials.GetAWSAccessKeyId()); auto crtCredentials = Aws::MakeShared(v4AsymmetricLogTag, Aws::Crt::ByteCursorFromCString(credentials.GetAWSAccessKeyId().c_str()), Aws::Crt::ByteCursorFromCString(credentials.GetAWSSecretKey().c_str()), diff --git a/src/aws-cpp-sdk-core/source/client/AWSClient.cpp b/src/aws-cpp-sdk-core/source/client/AWSClient.cpp index 8455b59847e2..8982cb35ff95 100644 --- a/src/aws-cpp-sdk-core/source/client/AWSClient.cpp +++ b/src/aws-cpp-sdk-core/source/client/AWSClient.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -262,6 +264,28 @@ static DateTime GetServerTimeFromError(const AWSError error) } } +void AWSClient::InvalidateCredentials(const AWSError& error, const Aws::String& accessKeyId) const +{ + if (!Aws::Internal::IsCredentialsRejectedError(error)) + { + return; + } + const Aws::String& errorCode = error.GetExceptionName(); + if (accessKeyId.empty()) + { + AWS_LOGSTREAM_DEBUG(AWS_CLIENT_LOG_TAG, "Not invalidating credentials after " << errorCode << ": the request was not signed with an access key."); + return; + } + const auto provider = GetCredentialsProvider(); + if (!provider) + { + AWS_LOGSTREAM_DEBUG(AWS_CLIENT_LOG_TAG, "Not invalidating credentials after " << errorCode << ": the client has no credentials provider."); + return; + } + AWS_LOGSTREAM_DEBUG(AWS_CLIENT_LOG_TAG, "Invalidating credentials after " << errorCode << "."); + provider->Invalidate(accessKeyId); +} + bool AWSClient::AdjustClockSkew(HttpResponseOutcome& outcome, const Aws::Utils::DateTime& timeRequestSent, const Aws::Utils::DateTime& timeResponseReceived, std::chrono::milliseconds attemptSkew) const { if (m_enableClockSkewAdjustment) @@ -354,6 +378,7 @@ HttpResponseOutcome AWSClient::AttemptExhaustively(const Aws::Http::URI& uri, break; } lastError = outcome.GetError(); + InvalidateCredentials(outcome.GetError(), httpRequest->GetSigningAccessKey()); DateTime serverTime = GetServerTimeFromError(outcome.GetError()); auto clockSkew = DateTime::Diff(serverTime, DateTime::Now()); @@ -534,6 +559,7 @@ HttpResponseOutcome AWSClient::AttemptExhaustively(const Aws::Http::URI& uri, break; } lastError = outcome.GetError(); + InvalidateCredentials(outcome.GetError(), httpRequest->GetSigningAccessKey()); DateTime serverTime = GetServerTimeFromError(outcome.GetError()); auto clockSkew = DateTime::Diff(serverTime, DateTime::Now()); @@ -942,6 +968,7 @@ Http::HttpClient::AttemptOutcome AWSClient::OnAttemptCompleteAsync(const std::sh } context->lastError = outcome.GetError(); + InvalidateCredentials(outcome.GetError(), context->httpRequest->GetSigningAccessKey()); DateTime serverTime = GetServerTimeFromError(outcome.GetError()); auto clockSkew = DateTime::Diff(serverTime, DateTime::Now()); diff --git a/src/aws-cpp-sdk-core/source/client/CoreErrors.cpp b/src/aws-cpp-sdk-core/source/client/CoreErrors.cpp index 62114d4c3279..8668ac5af8fb 100644 --- a/src/aws-cpp-sdk-core/source/client/CoreErrors.cpp +++ b/src/aws-cpp-sdk-core/source/client/CoreErrors.cpp @@ -94,6 +94,8 @@ void CoreErrorsMapper::InitCoreErrorsMapper() s_CoreErrorsMapper->emplace("RequestTimeTooSkewed", AWSError(CoreErrors::REQUEST_TIME_TOO_SKEWED, true)); s_CoreErrorsMapper->emplace("RequestTimeoutException", AWSError(CoreErrors::REQUEST_TIMEOUT, true)); s_CoreErrorsMapper->emplace("RequestTimeout", AWSError(CoreErrors::REQUEST_TIMEOUT, true)); + s_CoreErrorsMapper->emplace("ExpiredToken", AWSError(CoreErrors::EXPIRED_TOKEN, true)); + s_CoreErrorsMapper->emplace("InvalidToken", AWSError(CoreErrors::INVALID_TOKEN, true)); if (newRetriesEnabled) { s_CoreErrorsMapper->emplace("TooManyRequestsException", AWSError(CoreErrors::THROTTLING, RetryableType::RETRYABLE_THROTTLING)); s_CoreErrorsMapper->emplace("ProvisionedThroughputExceededException", AWSError(CoreErrors::THROTTLING, RetryableType::RETRYABLE_THROTTLING)); diff --git a/src/aws-cpp-sdk-core/source/config/EC2InstanceProfileConfigLoader.cpp b/src/aws-cpp-sdk-core/source/config/EC2InstanceProfileConfigLoader.cpp index 52dce0980fd4..0e67a02f035f 100644 --- a/src/aws-cpp-sdk-core/source/config/EC2InstanceProfileConfigLoader.cpp +++ b/src/aws-cpp-sdk-core/source/config/EC2InstanceProfileConfigLoader.cpp @@ -6,12 +6,12 @@ #include #include #include +#include #include #include #include #include #include -#include namespace Aws { @@ -21,12 +21,20 @@ namespace Aws using namespace Aws::Auth; static const char* const INTERNAL_EXCEPTION_PHRASE = "InternalServiceException"; - static const int64_t FIVE_MINUTE_MILLIS = 60000 * 5; - static const int64_t TEN_MINUTE_MILLIS = 60000 * 10; static const char* const EC2_INSTANCE_PROFILE_LOG_TAG = "Aws::Config::EC2InstanceProfileConfigLoader"; - EC2InstanceProfileConfigLoader::EC2InstanceProfileConfigLoader(const std::shared_ptr& client) + AWSCredentials EC2InstanceProfileConfigLoader::RetrieveForCache() + { + if (!RetrieveCredentials()) + { + return AWSCredentials(); + } + return m_profiles[INSTANCE_PROFILE_KEY].GetCredentials(); + } + + EC2InstanceProfileConfigLoader::EC2InstanceProfileConfigLoader(const std::shared_ptr& client) : + m_cachingProvider(Aws::MakeShared(EC2_INSTANCE_PROFILE_LOG_TAG, [this]() { return RetrieveForCache(); })) { if(client == nullptr) { @@ -39,7 +47,8 @@ namespace Aws } } - EC2InstanceProfileConfigLoader::EC2InstanceProfileConfigLoader(const Aws::Client::ClientConfiguration::CredentialProviderConfiguration& credentialConfig) + EC2InstanceProfileConfigLoader::EC2InstanceProfileConfigLoader(const Aws::Client::ClientConfiguration::CredentialProviderConfiguration& credentialConfig) : + m_cachingProvider(Aws::MakeShared(EC2_INSTANCE_PROFILE_LOG_TAG, [this]() { return RetrieveForCache(); })) { Aws::Internal::InitEC2MetadataClient(credentialConfig); m_ec2metadataClient = Aws::Internal::GetEC2MetadataClient(); @@ -47,14 +56,21 @@ namespace Aws bool EC2InstanceProfileConfigLoader::LoadInternal() { - // re-use old credentials until we need to call IMDS again. - if (DateTime::Now().Millis() < this->credentialsValidUntilMillis) { - AWS_LOGSTREAM_ERROR(EC2_INSTANCE_PROFILE_LOG_TAG, - "Skipping IMDS call until " << this->credentialsValidUntilMillis); - return true; - } - this->credentialsValidUntilMillis = DateTime::Now().Millis(); + return !GetCredentials().IsEmpty(); + } + AWSCredentials EC2InstanceProfileConfigLoader::GetCredentials() const + { + return m_cachingProvider->GetAWSCredentials(); + } + + void EC2InstanceProfileConfigLoader::Invalidate(const Aws::String& accessKeyId) + { + m_cachingProvider->Invalidate(accessKeyId); + } + + bool EC2InstanceProfileConfigLoader::RetrieveCredentials() + { if (!m_ec2metadataClient) { AWS_LOGSTREAM_FATAL(EC2_INSTANCE_PROFILE_LOG_TAG, "EC2MetadataClient is a nullptr!"); return false; @@ -82,13 +98,11 @@ namespace Aws if (expirationTime.WasParseSuccessful() && DateTime::Now() > expirationTime && m_profiles.find(INSTANCE_PROFILE_KEY) != m_profiles.end()) { AWS_LOGSTREAM_ERROR(EC2_INSTANCE_PROFILE_LOG_TAG, "Expiration Time of Credentials in the past, refusing to update credentials"); - this->credentialsValidUntilMillis = DateTime::Now().Millis() + calculateRetryTime(); - return true; + return false; } else if (credentialsView.GetString(code) == INTERNAL_EXCEPTION_PHRASE) { AWS_LOGSTREAM_ERROR(EC2_INSTANCE_PROFILE_LOG_TAG, "IMDS call failed, refusing to update credentials"); - this->credentialsValidUntilMillis = DateTime::Now().Millis() + calculateRetryTime(); - return true; + return false; } accessKey = credentialsView.GetString(accessKeyId); AWS_LOGSTREAM_INFO(EC2_INSTANCE_PROFILE_LOG_TAG, @@ -100,7 +114,9 @@ namespace Aws auto region = m_ec2metadataClient->GetCurrentRegion(); Profile profile; - profile.SetCredentials(AWSCredentials(accessKey, secretKey, token)); + profile.SetCredentials(expirationTime.WasParseSuccessful() + ? AWSCredentials(accessKey, secretKey, token, expirationTime) + : AWSCredentials(accessKey, secretKey, token)); profile.SetRegion(region); profile.SetName(INSTANCE_PROFILE_KEY); @@ -108,12 +124,5 @@ namespace Aws return true; } - - int64_t EC2InstanceProfileConfigLoader::calculateRetryTime() const { - std::random_device rd; - std::mt19937_64 gen(rd()); - std::uniform_int_distribution dist(FIVE_MINUTE_MILLIS, TEN_MINUTE_MILLIS); - return dist(gen); - } } // Config namespace } // Aws namespace diff --git a/src/aws-cpp-sdk-core/source/smithy/client/AwsSmithyClientBase.cpp b/src/aws-cpp-sdk-core/source/smithy/client/AwsSmithyClientBase.cpp index 8886461ed0db..950b4228fc26 100644 --- a/src/aws-cpp-sdk-core/source/smithy/client/AwsSmithyClientBase.cpp +++ b/src/aws-cpp-sdk-core/source/smithy/client/AwsSmithyClientBase.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -568,6 +569,23 @@ bool AwsSmithyClientBase::AdjustClockSkew(HttpResponseOutcome& outcome, const Aw return false; } +void AwsSmithyClientBase::InvalidateCredentials(const AWSCoreError& error, const AwsSmithyClientAsyncRequestContext& ctx) const +{ + if (!Aws::Internal::IsCredentialsRejectedError(error)) + { + return; + } + const Aws::String& errorCode = error.GetExceptionName(); + const Aws::String accessKeyId = ctx.m_httpRequest ? ctx.m_httpRequest->GetSigningAccessKey() : Aws::String(); + if (accessKeyId.empty()) + { + AWS_LOGSTREAM_DEBUG(AWS_SMITHY_CLIENT_LOG, "Not invalidating credentials after " << errorCode << ": the request was not signed with an access key."); + return; + } + AWS_LOGSTREAM_DEBUG(AWS_SMITHY_CLIENT_LOG, "Invalidating credentials after " << errorCode << "."); + InvalidateIdentity(ctx, accessKeyId); +} + Aws::Http::HttpClient::AttemptOutcome AwsSmithyClientBase::HandleAsyncReply(std::shared_ptr pRequestCtx, std::shared_ptr httpResponse) const { @@ -647,6 +665,7 @@ Aws::Http::HttpClient::AttemptOutcome AwsSmithyClientBase::HandleAsyncReply(std: break; } pRequestCtx->m_lastError = outcome.GetError(); + InvalidateCredentials(outcome.GetError(), *pRequestCtx); Utils::DateTime serverTime = Utils::GetServerTimeFromError(outcome.GetError()); auto clockSkew = Utils::DateTime::Diff(serverTime, Utils::DateTime::Now()); diff --git a/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/CognitoCachingCredentialsProvider.h b/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/CognitoCachingCredentialsProvider.h index 8eb655580aaf..b045623c556b 100644 --- a/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/CognitoCachingCredentialsProvider.h +++ b/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/CognitoCachingCredentialsProvider.h @@ -26,6 +26,11 @@ namespace Aws public: AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + protected: /** * PersistentCognitoIdentityProvider is responsible for storing and retrieving cognito identity information @@ -43,10 +48,14 @@ namespace Aws private: void OnLoginsUpdated(const PersistentCognitoIdentityProvider&); + AWSCredentials RetrieveCredentials(); AWSCredentials m_cachedCredentials; std::atomic m_expiry; std::mutex m_credsMutex; + std::atomic m_loginsGeneration{0}; + AWSCredentials RetrieveForCache(); + std::shared_ptr m_cachingProvider; }; /** diff --git a/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/STSAssumeRoleCredentialsProvider.h b/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/STSAssumeRoleCredentialsProvider.h index 8bb9da843e31..899afdd5bb44 100644 --- a/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/STSAssumeRoleCredentialsProvider.h +++ b/src/aws-cpp-sdk-identity-management/include/aws/identity-management/auth/STSAssumeRoleCredentialsProvider.h @@ -50,6 +50,11 @@ namespace Aws AWSCredentials GetAWSCredentials() override; + /** + * Invalidate marks the cached credentials for refresh if they match accessKeyId. + */ + void Invalidate(const Aws::String& accessKeyId) override; + private: void LoadCredentialsFromSTS(); @@ -61,6 +66,8 @@ namespace Aws std::atomic m_expiry; std::mutex m_credsMutex; std::atomic m_loadFrequency; + AWSCredentials RetrieveForCache(); + std::shared_ptr m_cachingProvider; }; } } diff --git a/src/aws-cpp-sdk-identity-management/source/auth/CognitoCachingCredentialsProvider.cpp b/src/aws-cpp-sdk-identity-management/source/auth/CognitoCachingCredentialsProvider.cpp index a1dd03b0d406..18e491a362dc 100644 --- a/src/aws-cpp-sdk-identity-management/source/auth/CognitoCachingCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-identity-management/source/auth/CognitoCachingCredentialsProvider.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,32 @@ using namespace Aws::Utils; static const char* LOG_TAG = "CognitoCachingCredentialsProvider"; static const char* MEM_TAG = "CognitoCachingCredentialsProvider"; +static const int MAX_LOGINS_CHANGED_RETRIES = 1; + +AWSCredentials CognitoCachingCredentialsProvider::RetrieveForCache() +{ + AWSCredentials credentials; + for (int attempt = 0; attempt <= MAX_LOGINS_CHANGED_RETRIES; ++attempt) + { + const uint64_t loginsGeneration = m_loginsGeneration.load(); + { + std::lock_guard locker(m_credsMutex); + m_cachedCredentials = AWSCredentials("", ""); + } + m_expiry.store(0.0); + credentials = RetrieveCredentials(); + if (m_loginsGeneration.load() == loginsGeneration) + { + break; + } + AWS_LOGSTREAM_INFO(LOG_TAG, "Logins were updated while fetching credentials, fetching again with the new logins."); + } + if (!credentials.IsEmpty()) + { + credentials.SetExpiration(DateTime(m_expiry.load())); + } + return credentials; +} CognitoCachingCredentialsProvider::CognitoCachingCredentialsProvider (const std::shared_ptr& identityRepository, const std::shared_ptr& cognitoIdentityClient) : @@ -25,12 +52,23 @@ CognitoCachingCredentialsProvider::CognitoCachingCredentialsProvider Aws::MakeShared(MEM_TAG, Aws::MakeShared(MEM_TAG))), m_identityRepository(identityRepository), m_cachedCredentials("", ""), - m_expiry(0.0) + m_expiry(0.0), + m_cachingProvider(Aws::MakeShared(MEM_TAG, [this]() { return RetrieveForCache(); })) { m_identityRepository->SetLoginsUpdatedCallback(std::bind(&CognitoCachingCredentialsProvider::OnLoginsUpdated, this, std::placeholders::_1)); } AWSCredentials CognitoCachingCredentialsProvider::GetAWSCredentials() +{ + return m_cachingProvider->GetAWSCredentials(); +} + +void CognitoCachingCredentialsProvider::Invalidate(const Aws::String& accessKeyId) +{ + m_cachingProvider->Invalidate(accessKeyId); +} + +AWSCredentials CognitoCachingCredentialsProvider::RetrieveCredentials() { if (IsTimeExpired(m_expiry.load())) { @@ -85,7 +123,14 @@ bool CognitoCachingCredentialsProvider::IsTimeExpired(double expiry) void CognitoCachingCredentialsProvider::OnLoginsUpdated(const PersistentCognitoIdentityProvider&) { AWS_LOGSTREAM_INFO(LOG_TAG, "Logins Updated in the identity repository, resetting the expiry to force a refresh on the next run."); + ++m_loginsGeneration; m_expiry.store(DateTime().SecondsWithMSPrecision()); + Aws::String accessKeyId; + { + std::lock_guard locker(m_credsMutex); + accessKeyId = m_cachedCredentials.GetAWSAccessKeyId(); + } + m_cachingProvider->Invalidate(accessKeyId); } diff --git a/src/aws-cpp-sdk-identity-management/source/auth/STSAssumeRoleCredentialsProvider.cpp b/src/aws-cpp-sdk-identity-management/source/auth/STSAssumeRoleCredentialsProvider.cpp index fc4c4e8a0331..bd78eb37bd10 100644 --- a/src/aws-cpp-sdk-identity-management/source/auth/STSAssumeRoleCredentialsProvider.cpp +++ b/src/aws-cpp-sdk-identity-management/source/auth/STSAssumeRoleCredentialsProvider.cpp @@ -4,6 +4,7 @@ */ #include +#include #include #include #include @@ -21,11 +22,25 @@ namespace Aws //60 seconds; static const int ACCOUNT_FOR_LATENCY = 60; + AWSCredentials STSAssumeRoleCredentialsProvider::RetrieveForCache() + { + m_cachedCredentials = AWSCredentials(); + m_expiry = 0; + LoadCredentialsFromSTS(); + AWSCredentials credentials = m_cachedCredentials; + if (!credentials.IsEmpty()) + { + credentials.SetExpiration(DateTime(m_expiry.load())); + } + return credentials; + } + STSAssumeRoleCredentialsProvider::STSAssumeRoleCredentialsProvider(const Aws::String& roleArn, const Aws::String& sessionName, const Aws::String& externalId, int loadFrequency, const std::shared_ptr& stsClient) : m_stsClient(stsClient == nullptr ? Aws::MakeShared(CLASS_TAG) : stsClient), m_roleArn(roleArn), m_sessionName(sessionName), m_externalId(externalId), - m_expiry(0), m_loadFrequency(loadFrequency) + m_expiry(0), m_loadFrequency(loadFrequency), + m_cachingProvider(Aws::MakeShared(CLASS_TAG, [this]() { return RetrieveForCache(); })) { if (sessionName.empty()) { @@ -38,9 +53,12 @@ namespace Aws AWSCredentials STSAssumeRoleCredentialsProvider::GetAWSCredentials() { - LoadCredentialsFromSTS(); - std::lock_guard locker(m_credsMutex); - return m_cachedCredentials; + return m_cachingProvider->GetAWSCredentials(); + } + + void STSAssumeRoleCredentialsProvider::Invalidate(const Aws::String& accessKeyId) + { + m_cachingProvider->Invalidate(accessKeyId); } void STSAssumeRoleCredentialsProvider::LoadCredentialsFromSTS() diff --git a/tests/aws-cpp-sdk-core-tests/CMakeLists.txt b/tests/aws-cpp-sdk-core-tests/CMakeLists.txt index e66641a7e076..6d511dc860a1 100644 --- a/tests/aws-cpp-sdk-core-tests/CMakeLists.txt +++ b/tests/aws-cpp-sdk-core-tests/CMakeLists.txt @@ -128,7 +128,8 @@ endif() target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBS} ${CLIENT_LIBS}) target_compile_definitions(${PROJECT_NAME} PRIVATE - "CLOCK_SKEW_TEST_CASES_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/resources/clock-skew-test-cases.json\"") + "CLOCK_SKEW_TEST_CASES_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/resources/clock-skew-test-cases.json\"" + "CREDENTIALS_REFRESH_TEST_CASES_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/resources/credentials-refresh-tests.json\"") add_custom_command(TARGET aws-cpp-sdk-core-tests PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/tests/aws-cpp-sdk-core-tests/aws/auth/AWSCredentialsProviderTest.cpp b/tests/aws-cpp-sdk-core-tests/aws/auth/AWSCredentialsProviderTest.cpp index 5e1e4409b3b5..836b567e2567 100644 --- a/tests/aws-cpp-sdk-core-tests/aws/auth/AWSCredentialsProviderTest.cpp +++ b/tests/aws-cpp-sdk-core-tests/aws/auth/AWSCredentialsProviderTest.cpp @@ -320,6 +320,19 @@ class InstanceProfileCredentialsProviderTest : public Aws::Testing::AwsCppSdkGTe { }; +// The credentials cache's refresh windows are relative to the credential's own expiration, not wall-clock +// time since the last poll, so tests that need a second real retrieve attempt use an expiration a couple +// seconds out: that's already within the 1 minute mandatory window by the time of the second call. +static Aws::String BuildIMDSCredentialsJson(const char* accessKeyId, const char* secretAccessKey, const char* token, + const DateTime& expiration) +{ + Aws::StringStream ss; + ss << R"({ "AccessKeyId": ")" << accessKeyId << R"(", "SecretAccessKey": ")" << secretAccessKey + << R"(", "Token": ")" << token << R"(", "Code": "Success", "Expiration": ")" + << expiration.ToGmtString(DateFormat::ISO_8601) << R"(" })"; + return ss.str(); +} + TEST_F(InstanceProfileCredentialsProviderTest, TestEC2MetadataClientReturnsGoodData) { auto mockClient = Aws::MakeShared(AllocationTag); @@ -337,15 +350,15 @@ TEST_F(InstanceProfileCredentialsProviderTest, TestThatProviderRefreshes) { auto mockClient = Aws::MakeShared(AllocationTag); - const char* validCredentials = R"({ "AccessKeyId": "goodAccessKey", "SecretAccessKey": "goodSecretKey", "Token": "goodToken", "Code": "Success", "Expiration": "2037-04-19T00:00:00Z" })"; - mockClient->SetMockedCredentialsValue(validCredentials); + DateTime firstExpiry(DateTime::Now().Millis() + 2000); + mockClient->SetMockedCredentialsValue(BuildIMDSCredentialsJson("goodAccessKey", "goodSecretKey", "goodToken", firstExpiry)); InstanceProfileCredentialsProvider provider(Aws::MakeShared(AllocationTag, mockClient), 10); ASSERT_EQ("goodAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); ASSERT_EQ("goodSecretKey", provider.GetAWSCredentials().GetAWSSecretKey()); - const char* nextSetOfCredentials = R"({ "AccessKeyId": "betterAccessKey", "SecretAccessKey": "betterSecretKey", "Token": "betterToken", "Code": "Success", "Expiration": "2037-04-19T00:00:00Z" })"; - mockClient->SetMockedCredentialsValue(nextSetOfCredentials); + DateTime nextExpiry(DateTime::Now().Millis() + 2000); + mockClient->SetMockedCredentialsValue(BuildIMDSCredentialsJson("betterAccessKey", "betterSecretKey", "betterToken", nextExpiry)); std::this_thread::sleep_for(std::chrono::milliseconds(100)); ASSERT_EQ("betterAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); ASSERT_EQ("betterSecretKey", provider.GetAWSCredentials().GetAWSSecretKey()); @@ -355,8 +368,8 @@ TEST_F(InstanceProfileCredentialsProviderTest, TestCredentialsDontRefreshForCred { auto mockClient = Aws::MakeShared(AllocationTag); - const char* validCredentials = R"({ "AccessKeyId": "goodAccessKey", "SecretAccessKey": "goodSecretKey", "Token": "goodToken", "Code": "Success", "Expiration": "2037-04-19T00:00:00Z" })"; - mockClient->SetMockedCredentialsValue(validCredentials); + DateTime firstExpiry(DateTime::Now().Millis() + 2000); + mockClient->SetMockedCredentialsValue(BuildIMDSCredentialsJson("goodAccessKey", "goodSecretKey", "goodToken", firstExpiry)); InstanceProfileCredentialsProvider provider(Aws::MakeShared(AllocationTag, mockClient), 10); ASSERT_EQ("goodAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); @@ -374,8 +387,8 @@ TEST_F(InstanceProfileCredentialsProviderTest, TestCredentialsDontRefreshForFail { auto mockClient = Aws::MakeShared(AllocationTag); - const char* validCredentials = R"({ "AccessKeyId": "goodAccessKey", "SecretAccessKey": "goodSecretKey", "Token": "goodToken", "Code": "Success", "Expiration": "2037-04-19T00:00:00Z" })"; - mockClient->SetMockedCredentialsValue(validCredentials); + DateTime firstExpiry(DateTime::Now().Millis() + 2000); + mockClient->SetMockedCredentialsValue(BuildIMDSCredentialsJson("goodAccessKey", "goodSecretKey", "goodToken", firstExpiry)); InstanceProfileCredentialsProvider provider(Aws::MakeShared(AllocationTag, mockClient), 10); ASSERT_EQ("goodAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); @@ -428,15 +441,18 @@ TEST_F(InstanceProfileCredentialsProviderTest, TestEC2MetadataClientReturnsBadDa ASSERT_EQ("", provider.GetAWSCredentials().GetAWSSecretKey()); } -TEST_F(InstanceProfileCredentialsProviderTest, TestUsesExpiredCredentialsIfNoneExist) { +// The credentials cache treats a retrieve that comes back already expired as a failure rather than a +// cacheable success, even with nothing else to serve: expired credentials would just be rejected by +// the service anyway, so there is no benefit to handing them out. +TEST_F(InstanceProfileCredentialsProviderTest, TestDoesNotUseAlreadyExpiredCredentialsIfNoneExist) { auto mockClient = Aws::MakeShared(AllocationTag); const char* validCredentials = R"({ "AccessKeyId": "goodAccessKey", "SecretAccessKey": "goodSecretKey", "Token": "goodToken", "Code": "Success", "Expiration": "1991-04-19T06:12:00Z" })"; mockClient->SetMockedCredentialsValue(validCredentials); InstanceProfileCredentialsProvider provider(Aws::MakeShared(AllocationTag, mockClient), 10); - ASSERT_EQ("goodAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); - ASSERT_EQ("goodSecretKey", provider.GetAWSCredentials().GetAWSSecretKey()); + ASSERT_EQ("", provider.GetAWSCredentials().GetAWSAccessKeyId()); + ASSERT_EQ("", provider.GetAWSCredentials().GetAWSSecretKey()); } static Aws::String WrapEchoStringWithSingleQuoteForUnixShell(Aws::String str) diff --git a/tests/aws-cpp-sdk-core-tests/aws/auth/GeneralHTTPCredentialsProviderTest.cpp b/tests/aws-cpp-sdk-core-tests/aws/auth/GeneralHTTPCredentialsProviderTest.cpp index 79143f3391c6..f4bb863fea77 100644 --- a/tests/aws-cpp-sdk-core-tests/aws/auth/GeneralHTTPCredentialsProviderTest.cpp +++ b/tests/aws-cpp-sdk-core-tests/aws/auth/GeneralHTTPCredentialsProviderTest.cpp @@ -437,7 +437,7 @@ const char GeneralHTTPCredentialsProviderParamsTest::RH_TEST_CASES[] = R"([ "headers": { "Content-Type": "application/json" }, - "body": "{\"Foo\":\"Bar\",\"AccessKeyId\":\"foo\",\"SecretAccessKey\":\"bar\",\"Token\":\"baz\",\"AccountId\":\"qux\",\"Expiration\":\"2023-06-30T00:00:00.000Z\"}" + "body": "{\"Foo\":\"Bar\",\"AccessKeyId\":\"foo\",\"SecretAccessKey\":\"bar\",\"Token\":\"baz\",\"AccountId\":\"qux\",\"Expiration\":\"2037-06-30T00:00:00.000Z\"}" }, "expect": { "type": "success", @@ -446,7 +446,7 @@ const char GeneralHTTPCredentialsProviderParamsTest::RH_TEST_CASES[] = R"([ "secret_access_key": "bar", "session_token": "baz", "account_id": "qux", - "expiration": "2023-06-30T00:00:00.000Z" + "expiration": "2037-06-30T00:00:00.000Z" } } } @@ -588,7 +588,7 @@ TEST_F(TaskRoleCredentialsProviderTest, TestECSCredentialsClientReturnsGoodData) { auto mockClient = Aws::MakeShared(ALLOCATION_TAG, "/path/to/res"); - const char* validCredentials = "{ \"AccessKeyId\": \"goodAccessKey\", \"SecretAccessKey\": \"goodSecretKey\", \"Token\": \"goodToken\", \"Expiration\": \"2020-02-25T06:03:31Z\" }"; + const char* validCredentials = "{ \"AccessKeyId\": \"goodAccessKey\", \"SecretAccessKey\": \"goodSecretKey\", \"Token\": \"goodToken\", \"Expiration\": \"2037-02-25T06:03:31Z\" }"; mockClient->SetMockedCredentialsValue(validCredentials); TaskRoleCredentialsProvider provider(mockClient, 1000 * 60 * 15); @@ -603,16 +603,12 @@ TEST_F(TaskRoleCredentialsProviderTest, TestThatProviderRefreshes) Aws::String goodCredentialsPrefix("{ \"AccessKeyId\": \"goodAccessKey\", \"SecretAccessKey\": \"goodSecretKey\", \"Token\": \"goodToken\", \"Expiration\": "); Aws::String betterCredentialsPrefix("{ \"AccessKeyId\": \"betterAccessKey\", \"SecretAccessKey\": \"betterSecretKey\", \"Token\": \"betterToken\", \"Expiration\": "); - DateTime now = DateTime::Now(); - Aws::String dateStringNow = now.ToGmtString(DateFormat::ISO_8601); - DateTime after = now.Millis() + 1000; - Aws::String dateStringAfter = after.ToGmtString(DateFormat::ISO_8601); - - // Set the current credentials expiration date to now, which expires immediately. - // Next time when calling GetAWSCredentials, the credentials will be refreshed. + // Expiration must be strictly in the future for the credentials cache to accept the retrieve, but close + // enough that the second call below already falls within the cache's 1 minute mandatory refresh window. + DateTime firstExpiry(DateTime::Now().Millis() + 2000); Aws::StringStream validCredentials; - validCredentials << goodCredentialsPrefix << "\"" << dateStringNow << "\" }"; + validCredentials << goodCredentialsPrefix << "\"" << firstExpiry.ToGmtString(DateFormat::ISO_8601) << "\" }"; mockClient->SetMockedCredentialsValue(validCredentials.str()); @@ -620,8 +616,9 @@ TEST_F(TaskRoleCredentialsProviderTest, TestThatProviderRefreshes) ASSERT_EQ("goodAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); ASSERT_EQ("goodSecretKey", provider.GetAWSCredentials().GetAWSSecretKey()); + DateTime nextExpiry(DateTime::Now().Millis() + 2000); Aws::StringStream nextSetOfCredentials; - nextSetOfCredentials << betterCredentialsPrefix << "\"" << dateStringAfter << "\" }"; + nextSetOfCredentials << betterCredentialsPrefix << "\"" << nextExpiry.ToGmtString(DateFormat::ISO_8601) << "\" }"; mockClient->SetMockedCredentialsValue(nextSetOfCredentials.str()); ASSERT_EQ("betterAccessKey", provider.GetAWSCredentials().GetAWSAccessKeyId()); diff --git a/tests/aws-cpp-sdk-core-tests/resources/credentials-refresh-tests.json b/tests/aws-cpp-sdk-core-tests/resources/credentials-refresh-tests.json new file mode 100644 index 000000000000..ed2e4a9d97a3 --- /dev/null +++ b/tests/aws-cpp-sdk-core-tests/resources/credentials-refresh-tests.json @@ -0,0 +1,373 @@ +[ + { + "documentation": "Valid cached credentials: no refresh is attempted and the caller receives the cached credentials.", + "given": { "cachedCredentials": "valid" }, + "steps": [ + { + "type": "getCredentials", + "expected": { "result": "cachedCredentials", "sourceContacted": false, "rateLimited": false } + } + ] + }, + { + "documentation": "Advisory window, refresh succeeds: the caller receives the newly refreshed credentials.", + "given": { "cachedCredentials": "advisory" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Advisory window, refresh fails: the resolver applies the refresh backoff and the caller receives the existing cached credentials.", + "given": { "cachedCredentials": "advisory" }, + "steps": [ + { + "type": "getCredentials", + "response": "error", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Mandatory window, refresh succeeds: the caller receives the newly refreshed credentials.", + "given": { "cachedCredentials": "mandatory" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Mandatory window, refresh fails: the resolver applies the refresh backoff and the caller receives the cached credentials.", + "given": { "cachedCredentials": "mandatory" }, + "steps": [ + { + "type": "getCredentials", + "response": "error", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Expired credentials are refreshed successfully: the caller receives the newly refreshed credentials.", + "given": { "cachedCredentials": "expired" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Expired credentials, refresh fails: the resolver applies the refresh backoff and the caller receives the expired cached credentials rather than raising.", + "given": { "cachedCredentials": "expired" }, + "steps": [ + { + "type": "getCredentials", + "response": "error", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "No cached credentials and the initial fetch fails: the SDK raises, since there are no cached credentials to fall back on. The next call retries and succeeds.", + "given": { "cachedCredentials": "none" }, + "steps": [ + { + "type": "getCredentials", + "response": "error", + "expected": { "result": "noCredentialsError", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Advisory window, source returns stale credentials (Expiration at or before now): treated as a failed refresh. The resolver applies the refresh backoff and returns the existing cached credentials.", + "given": { "cachedCredentials": "advisory" }, + "steps": [ + { + "type": "getCredentials", + "response": "staleCredentials", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Mandatory window, source returns stale credentials: same as the advisory case, treated as a failed refresh.", + "given": { "cachedCredentials": "mandatory" }, + "steps": [ + { + "type": "getCredentials", + "response": "staleCredentials", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + + { + "documentation": "A 10-minute credential lifetime selects the 5-minute advisory window (lifetime <= 20 minutes).", + "given": { "cachedCredentials": "none" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "lifetimeSeconds": 600, + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 300 } + } + ] + }, + { + "documentation": "A 20.5-minute credential lifetime selects the 15-minute advisory window (lifetime > 20 and < 90 minutes).", + "given": { "cachedCredentials": "none" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "lifetimeSeconds": 1230, + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 900 } + } + ] + }, + { + "documentation": "A 6-hour credential lifetime selects the 60-minute advisory window (lifetime >= 90 minutes).", + "given": { "cachedCredentials": "none" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "lifetimeSeconds": 21600, + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 3600 } + } + ] + }, + { + "documentation": "After a successful refresh returns credentials with a different lifetime, the SDK recomputes the advisory window. The first credentials have a 6-hour lifetime (60-minute window); after advancing into that window, the refreshed credentials have a 10-minute lifetime (5-minute window).", + "given": { "cachedCredentials": "none" }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "lifetimeSeconds": 21600, + "documentation": "Initial fetch returns 6-hour credentials, selecting the 60-minute advisory window.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 3600 } + }, + { + "type": "advanceTime", + "seconds": 18060 + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "lifetimeSeconds": 600, + "documentation": "59 minutes remain until expiration, inside the 60-minute advisory window, so the SDK refreshes. The new 10-minute credentials select the 5-minute advisory window.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 300 } + } + ] + }, + { + "documentation": "A customer-configured advisory window overrides the table. Credentials with a 6-hour lifetime would map to 60 minutes, but the configured 30-minute window is used instead.", + "given": { "cachedCredentials": "none", "configuredAdvisoryWindowSeconds": 1800 }, + "steps": [ + { + "type": "getCredentials", + "response": "freshCredentials", + "lifetimeSeconds": 21600, + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false, "advisoryWindowSeconds": 1800 } + } + ] + }, + + { + "documentation": "Advisory window, non-recoverable failure: the SDK raises immediately. No refresh backoff is applied, but the error is cached for up to 5 seconds, so a recovering call succeeds once that cache expires.", + "given": { "cachedCredentials": "advisory" }, + "steps": [ + { + "type": "getCredentials", + "response": "nonRecoverableError", + "documentation": "Non-recoverable failure: the SDK raises and does not apply the refresh backoff.", + "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "advanceTime", + "seconds": 6 + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "documentation": "The non-recoverable error cache (max 5 seconds) has expired, so this call contacts the source again and succeeds.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Mandatory window, non-recoverable failure: the SDK raises immediately. No refresh backoff is applied, but the error is cached for up to 5 seconds, so a recovering call succeeds once that cache expires.", + "given": { "cachedCredentials": "mandatory" }, + "steps": [ + { + "type": "getCredentials", + "response": "nonRecoverableError", + "documentation": "Non-recoverable failure: the SDK raises and does not apply the refresh backoff.", + "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "advanceTime", + "seconds": 6 + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "documentation": "The non-recoverable error cache (max 5 seconds) has expired, so this call contacts the source again and succeeds.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Non-recoverable error, then an immediate retry with no clock advance: the error is still cached, so the SDK re-raises it without contacting the source. This protects the credential source from an application that swallows the error and retries in a loop.", + "given": { "cachedCredentials": "advisory" }, + "steps": [ + { + "type": "getCredentials", + "response": "nonRecoverableError", + "documentation": "Non-recoverable failure: the SDK raises and caches the error for up to 5 seconds.", + "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "getCredentials", + "documentation": "Immediate retry with no clock advance. The cached error is still active, so the SDK re-raises it without contacting the source.", + "expected": { "result": "nonRecoverableError", "sourceContacted": false, "rateLimited": false } + } + ] + }, + + { + "documentation": "Invalidate with an access key ID matching the cached credentials routes the next getCredentials through the mandatory refresh path, and the refresh succeeds.", + "given": { "cachedCredentials": "valid", "accessKeyId": "AKID-1" }, + "steps": [ + { "type": "invalidate", "rejectedAccessKeyId": "AKID-1" }, + { + "type": "getCredentials", + "response": "freshCredentials", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Invalidate with a matching access key ID routes the next getCredentials through the mandatory refresh path; the refresh fails and the SDK continues using the cached credentials.", + "given": { "cachedCredentials": "valid", "accessKeyId": "AKID-1" }, + "steps": [ + { "type": "invalidate", "rejectedAccessKeyId": "AKID-1" }, + { + "type": "getCredentials", + "response": "error", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Invalidate during an active backoff: the SDK does not contact the credential source. Once the refresh backoff has elapsed, the next getCredentials attempts a refresh.", + "given": { "cachedCredentials": "expired", "accessKeyId": "AKID-1", "refreshBackoffSeconds": 420 }, + "steps": [ + { + "type": "getCredentials", + "response": "error", + "documentation": "Refresh fails, so the SDK applies the refresh backoff.", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "advanceTime", + "seconds": 60 + }, + { "type": "invalidate", "rejectedAccessKeyId": "AKID-1" }, + { + "type": "getCredentials", + "documentation": "60s elapsed and the refresh backoff has not yet elapsed, so even after invalidation the SDK does not contact the credential source.", + "expected": { "result": "cachedCredentials", "sourceContacted": false, "rateLimited": true } + }, + { + "type": "advanceTime", + "seconds": 425 + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "documentation": "485s elapsed total and the refresh backoff has elapsed, so the SDK contacts the credential source and the refresh succeeds.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "Invalidate with a stale access key ID (a concurrent refresh already replaced the credentials): the cache is unchanged and the next getCredentials does not contact the source.", + "given": { "cachedCredentials": "valid", "accessKeyId": "AKID-2" }, + "steps": [ + { "type": "invalidate", "rejectedAccessKeyId": "AKID-1" }, + { + "type": "getCredentials", + "expected": { "result": "cachedCredentials", "sourceContacted": false, "rateLimited": false } + } + ] + }, + + { + "documentation": "After a failed refresh, the SDK does not contact the credential source again until the refresh backoff has elapsed.", + "given": { "cachedCredentials": "expired", "refreshBackoffSeconds": 420 }, + "steps": [ + { + "type": "getCredentials", + "response": "error", + "documentation": "Refresh fails, so the SDK applies the refresh backoff.", + "expected": { "result": "cachedCredentials", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "advanceTime", + "seconds": 300 + }, + { + "type": "getCredentials", + "documentation": "300s elapsed and the refresh backoff has not yet elapsed, so the SDK does not contact the credential source.", + "expected": { "result": "cachedCredentials", "sourceContacted": false, "rateLimited": true } + }, + { + "type": "advanceTime", + "seconds": 425 + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "documentation": "725s elapsed total and the refresh backoff has elapsed, so the SDK contacts the credential source and the refresh succeeds.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + }, + { + "documentation": "No cached credentials and the initial fetch fails with a non-recoverable error: the SDK raises the error directly rather than a generic NoCredentialsError. No refresh backoff is applied, but the error is cached for up to 5 seconds, so a recovering call succeeds once that cache expires.", + "given": { "cachedCredentials": "none" }, + "steps": [ + { + "type": "getCredentials", + "response": "nonRecoverableError", + "documentation": "Non-recoverable failure: the SDK raises and does not apply the refresh backoff.", + "expected": { "result": "nonRecoverableError", "sourceContacted": true, "rateLimited": false } + }, + { + "type": "advanceTime", + "seconds": 6 + }, + { + "type": "getCredentials", + "response": "freshCredentials", + "documentation": "The non-recoverable error cache (max 5 seconds) has expired, so this call contacts the source again and succeeds.", + "expected": { "result": "newCredentials", "sourceContacted": true, "rateLimited": false } + } + ] + } +] diff --git a/tests/aws-cpp-sdk-core-tests/utils/CredentialsCachingTest.cpp b/tests/aws-cpp-sdk-core-tests/utils/CredentialsCachingTest.cpp new file mode 100644 index 000000000000..c60618d35aea --- /dev/null +++ b/tests/aws-cpp-sdk-core-tests/utils/CredentialsCachingTest.cpp @@ -0,0 +1,358 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +// Runs the vendored credentials-refresh test cases (resources/credentials-refresh-tests.json) through the +// refresh state machine. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Aws::Internal; +using Aws::Utils::DateTime; +using Aws::Utils::Json::JsonValue; +using Aws::Utils::Json::JsonView; +using Aws::Internal::RefreshResult; + +namespace +{ + struct Creds { Aws::String akid; }; + using Result = RefreshResult; + using Cache = CredentialsCachingState; + + // Fixed jitter so the pinned durations are exact: backoff = 420 s, non-recoverable cache = 2.6 s. + const double JITTER = 0.4; + + std::shared_ptr StaticStability() + { + return Aws::MakeShared("CredentialsCachingTest"); + } + + Aws::String ReadFileContents(const char* path) + { + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + return Aws::String(buffer.str().c_str()); + } + + // SepAdvisoryWindow is the SEP's advisory window for a credential lifetime. + std::chrono::seconds SepAdvisoryWindow(std::chrono::seconds lifetime) + { + if (lifetime <= std::chrono::minutes(20)) { return std::chrono::minutes(5); } + if (lifetime < std::chrono::minutes(90)) { return std::chrono::minutes(15); } + return std::chrono::minutes(60); + } + + // IssuedCredentials is what the test's source handed out, so the test can tell when they are due for refresh. + struct IssuedCredentials + { + DateTime expiration; + std::chrono::seconds lifetime; + }; + + // ExpectAdvisoryWindow checks that credentials with this lifetime start refreshing exactly windowSeconds before expiry. + void ExpectAdvisoryWindow(int lifetimeSeconds, int windowSeconds) + { + auto now = std::make_shared(DateTime::Now()); + auto next = std::make_shared(Result::Success(Creds{"PROBE"}, *now + std::chrono::seconds(lifetimeSeconds))); + auto calls = std::make_shared(0); + Cache cache([next, calls]() { ++*calls; return *next; }, + [](const Creds& credentials) { return credentials.akid; }, + StaticStability(), + [now]() { return *now; }, + []() { return JITTER; }); + cache.GetCredentials(); + *next = Result::Success(Creds{"PROBE-2"}, *now + std::chrono::seconds(lifetimeSeconds * 2)); + + *now = *now + std::chrono::seconds(lifetimeSeconds - windowSeconds - 1); + cache.GetCredentials(); + EXPECT_EQ(1, *calls) << "refreshed before the " << windowSeconds << "s advisory window"; + + *now = *now + std::chrono::seconds(1); + cache.GetCredentials(); + EXPECT_EQ(2, *calls) << "did not refresh at the start of the " << windowSeconds << "s advisory window"; + } + + const int CONCURRENT_CALLERS = 8; + const std::chrono::seconds CALLER_TIMEOUT{5}; + + // BlockingSource hands out next, and once blocking is set, holds each retrieve until Release() is called. + class BlockingSource + { + public: + Result Retrieve() + { + ++calls; + std::unique_lock lock(m_mutex); + if (blocking) + { + m_entered = true; + m_cv.notify_all(); + m_cv.wait(lock, [this]() { return m_released; }); + } + return next; + } + + bool WaitUntilEntered() + { + std::unique_lock lock(m_mutex); + return m_cv.wait_for(lock, CALLER_TIMEOUT, [this]() { return m_entered; }); + } + + void Release() + { + std::lock_guard lock(m_mutex); + m_released = true; + m_cv.notify_all(); + } + + std::atomic calls{0}; + bool blocking = false; + Result next = Result::Recoverable("unscripted"); + + private: + std::mutex m_mutex; + std::condition_variable m_cv; + bool m_entered = false; + bool m_released = false; + }; + + // SeedCache caches 60-minute SEED credentials, then moves the clock forward by elapsed and makes the source block. + void SeedCache(Cache& cache, BlockingSource& source, DateTime& now, std::chrono::seconds elapsed) + { + source.next = Result::Success(Creds{"SEED"}, now + std::chrono::minutes(60)); + cache.GetCredentials(); + now = now + elapsed; + source.next = Result::Success(Creds{"NEW"}, now + std::chrono::minutes(60)); + source.blocking = true; + } + + Aws::Vector>> StartCallers(Cache& cache) + { + Aws::Vector>> callers; + for (int i = 0; i < CONCURRENT_CALLERS; ++i) + { + callers.push_back(std::async(std::launch::async, [&cache]() { return cache.GetCredentials(); })); + } + return callers; + } +} + +class CredentialsCachingTest : public Aws::Testing::AwsCppSdkGTestSuite {}; + +TEST_F(CredentialsCachingTest, RunsSepTestCases) +{ + const Aws::String contents = ReadFileContents(CREDENTIALS_REFRESH_TEST_CASES_PATH); + JsonValue document(contents); + ASSERT_TRUE(document.WasParseSuccessful()) << "failed to parse " << CREDENTIALS_REFRESH_TEST_CASES_PATH; + const auto cases = document.View().AsArray(); + ASSERT_GT(cases.GetLength(), 0u); + + for (size_t i = 0; i < cases.GetLength(); ++i) + { + const JsonView testCase = cases.GetItem(i); + SCOPED_TRACE(testCase.GetString("documentation").c_str()); + const JsonView given = testCase.GetObject("given"); + // No configurable refresh window in C++; log + continue (a per-case GTEST_SKIP would abort the loop). + if (given.KeyExists("configuredAdvisoryWindowSeconds")) + { + GTEST_LOG_(INFO) << "Skipping corpus case (no configurable refresh window): " << testCase.GetString("documentation"); + continue; + } + const auto allSteps = testCase.GetArray("steps"); + const Aws::String initialState = given.GetString("cachedCredentials"); + const Aws::String seedAkid = given.KeyExists("accessKeyId") ? given.GetString("accessKeyId") + : Aws::String("AKID-seed"); + + auto now = std::make_shared(DateTime::Now()); + auto next = std::make_shared(Result::Recoverable("unscripted")); + auto calls = std::make_shared(0); + auto handedOut = std::make_shared(); + Cache cache([next, calls, handedOut]() + { + ++*calls; + if (next->status == Result::Status::Success) { *handedOut = next->credentials.akid; } + return *next; + }, + [](const Creds& credentials) { return credentials.akid; }, + StaticStability(), + [now]() { return *now; }, + []() { return JITTER; }); + + Aws::Map issued; + Aws::String cachedAkid = seedAkid; + int freshCount = 0; + + // Seed the initial cache state with a 60-minute credential (15-minute advisory window), then + // advance the clock so the credentials land in the requested window. + if (initialState != "none") + { + *next = Result::Success(Creds{seedAkid}, *now + std::chrono::minutes(60)); + issued[seedAkid] = IssuedCredentials{*now + std::chrono::minutes(60), std::chrono::minutes(60)}; + cache.GetCredentials(); + if (initialState == "advisory") { *now = *now + std::chrono::minutes(50); } + else if (initialState == "mandatory") { *now = *now + std::chrono::minutes(59) + std::chrono::seconds(30); } + else if (initialState == "expired") { *now = *now + std::chrono::minutes(61); } + // "valid": leave the clock at the retrieve time. + } + + for (size_t s = 0; s < allSteps.GetLength(); ++s) + { + const JsonView step = allSteps.GetItem(s); + const Aws::String type = step.GetString("type"); + + if (type == "advanceTime") + { + *now = *now + std::chrono::seconds(step.GetInteger("seconds")); + continue; + } + if (type == "invalidate") + { + const Aws::String rejectedAkid = step.GetString("rejectedAccessKeyId"); + cache.Invalidate(rejectedAkid); + if (rejectedAkid == cachedAkid && issued.count(cachedAkid)) { issued[cachedAkid].expiration = *now; } + continue; + } + + // getCredentials: program what the source returns on this call (if it is contacted). + if (step.KeyExists("response")) + { + const Aws::String response = step.GetString("response"); + if (response == "freshCredentials") + { + const int lifetimeSeconds = step.KeyExists("lifetimeSeconds") ? step.GetInteger("lifetimeSeconds") : 3600; + const Aws::String akid = "NEW-" + Aws::Utils::StringUtils::to_string(++freshCount); + *next = Result::Success(Creds{akid}, *now + std::chrono::seconds(lifetimeSeconds)); + issued[akid] = IssuedCredentials{*now + std::chrono::seconds(lifetimeSeconds), std::chrono::seconds(lifetimeSeconds)}; + } + else if (response == "staleCredentials") + { + *next = Result::Success(Creds{"STALE"}, *now + std::chrono::seconds(-1)); // Expiration at/before now + } + else if (response == "error") + { + *next = Result::Recoverable("recoverable"); + } + else if (response == "nonRecoverableError") + { + *next = Result::NonRecoverable("non-recoverable"); + } + } + + const int callsBefore = *calls; + handedOut->clear(); + const auto outcome = cache.GetCredentials(); + const bool sourceContacted = *calls > callsBefore; + + const JsonView expected = step.GetObject("expected"); + Aws::String actualResult; + bool rateLimited = false; + if (outcome.IsSuccess()) + { + const Aws::String& akid = outcome.GetResult().akid; + actualResult = sourceContacted && akid == *handedOut ? "newCredentials" : "cachedCredentials"; + if (!sourceContacted && issued.count(akid)) + { + const IssuedCredentials& creds = issued[akid]; + rateLimited = now->Millis() >= creds.expiration.Millis() - + std::chrono::duration_cast(SepAdvisoryWindow(creds.lifetime)).count(); + } + cachedAkid = akid; + } + else + { + actualResult = outcome.GetError() == "non-recoverable" ? "nonRecoverableError" : "noCredentialsError"; + } + + EXPECT_EQ(expected.GetString("result"), actualResult) << "step " << s; + EXPECT_EQ(expected.GetBool("sourceContacted"), sourceContacted) << "step " << s; + if (expected.KeyExists("rateLimited")) + { + EXPECT_EQ(expected.GetBool("rateLimited"), rateLimited) << "step " << s; + } + if (expected.KeyExists("advisoryWindowSeconds") && step.KeyExists("lifetimeSeconds")) + { + ExpectAdvisoryWindow(step.GetInteger("lifetimeSeconds"), expected.GetInteger("advisoryWindowSeconds")); + } + } + } +} + +TEST_F(CredentialsCachingTest, ConcurrentAdvisoryCallersDoNotWaitForRefresh) +{ + BlockingSource source; + DateTime now = DateTime::Now(); + Cache cache([&source]() { return source.Retrieve(); }, + [](const Creds& credentials) { return credentials.akid; }, + StaticStability(), + [&now]() { return now; }, + []() { return JITTER; }); + SeedCache(cache, source, now, std::chrono::minutes(50)); + + auto refresher = std::async(std::launch::async, [&cache]() { return cache.GetCredentials(); }); + ASSERT_TRUE(source.WaitUntilEntered()); + + auto callers = StartCallers(cache); + for (auto& caller : callers) + { + if (caller.wait_for(CALLER_TIMEOUT) != std::future_status::ready) + { + source.Release(); + FAIL() << "an advisory caller waited for the in-flight refresh"; + } + const auto outcome = caller.get(); + ASSERT_TRUE(outcome.IsSuccess()); + EXPECT_EQ("SEED", outcome.GetResult().akid); + } + + source.Release(); + const auto refreshed = refresher.get(); + ASSERT_TRUE(refreshed.IsSuccess()); + EXPECT_EQ("NEW", refreshed.GetResult().akid); + EXPECT_EQ(2, source.calls.load()); +} + +TEST_F(CredentialsCachingTest, ConcurrentMandatoryCallersShareOneRefresh) +{ + BlockingSource source; + DateTime now = DateTime::Now(); + Cache cache([&source]() { return source.Retrieve(); }, + [](const Creds& credentials) { return credentials.akid; }, + StaticStability(), + [&now]() { return now; }, + []() { return JITTER; }); + SeedCache(cache, source, now, std::chrono::minutes(59) + std::chrono::seconds(30)); + + auto callers = StartCallers(cache); + ASSERT_TRUE(source.WaitUntilEntered()); + for (auto& caller : callers) + { + EXPECT_EQ(std::future_status::timeout, caller.wait_for(std::chrono::milliseconds(50))) + << "a mandatory caller returned before the refresh finished"; + } + + source.Release(); + for (auto& caller : callers) + { + const auto outcome = caller.get(); + ASSERT_TRUE(outcome.IsSuccess()); + EXPECT_EQ("NEW", outcome.GetResult().akid); + } + EXPECT_EQ(2, source.calls.load()); +} diff --git a/tests/aws-cpp-sdk-identity-management-tests/auth/STSAssumeRoleCredentialsProviderTest.cpp b/tests/aws-cpp-sdk-identity-management-tests/auth/STSAssumeRoleCredentialsProviderTest.cpp index 48787a785f30..1e23f8271ed7 100644 --- a/tests/aws-cpp-sdk-identity-management-tests/auth/STSAssumeRoleCredentialsProviderTest.cpp +++ b/tests/aws-cpp-sdk-identity-management-tests/auth/STSAssumeRoleCredentialsProviderTest.cpp @@ -69,7 +69,9 @@ TEST(STSAssumeRoleCredentialsProviderTest, TestCredentialsLoadAndCache) { auto stsClient = Aws::MakeShared(CLASS_TAG); - DateTime expiryTime(DateTime::CurrentTimeMillis() + 70000); + // Expiry must sit outside the credentials cache's 5 minute advisory window (for lifetimes <= 20 min), + // otherwise the second call below would be treated as due for a refresh. + DateTime expiryTime(DateTime::CurrentTimeMillis() + 600000); Model::Credentials stsCredentials; stsCredentials.WithAccessKeyId(ACCESS_KEY_ID_1)