1+ // SSOCredentialTrackingTest.cpp
2+
3+ #include < aws/testing/AwsCppSdkGTestSuite.h>
4+ #include < aws/testing/mocks/aws/client/MockAWSClient.h>
5+ #include < aws/testing/mocks/http/MockHttpClient.h>
6+ #include < aws/testing/platform/PlatformTesting.h>
7+
8+ #include < aws/core/auth/AWSCredentialsProvider.h>
9+ #include < aws/core/auth/AWSCredentialsProviderChain.h>
10+ #include < aws/core/auth/SSOCredentialsProvider.h>
11+ #include < aws/core/client/AWSClient.h>
12+ #include < aws/core/http/standard/StandardHttpResponse.h>
13+ #include < aws/core/platform/Environment.h>
14+ #include < aws/core/platform/FileSystem.h>
15+ #include < aws/core/utils/DateTime.h>
16+ #include < aws/core/utils/HashingUtils.h>
17+ #include < aws/core/utils/StringUtils.h>
18+ #include < aws/core/config/AWSProfileConfigLoader.h>
19+
20+ #include < fstream>
21+ #include < thread>
22+
23+ using namespace Aws ;
24+ using namespace Aws ::Auth;
25+ using namespace Aws ::Client;
26+ using namespace Aws ::Http;
27+ using namespace Aws ::Http::Standard;
28+ using namespace Aws ::Utils;
29+ using namespace Aws ::FileSystem;
30+
31+ namespace {
32+ const char * TEST_LOG_TAG = " CredentialTrackingTest" ;
33+ }
34+
35+ Aws::String computeHashedStartUrl (const Aws::String& startUrl) {
36+ auto sha1 = HashingUtils::CalculateSHA1 (startUrl);
37+ return HashingUtils::HexEncode (sha1); // lower-case hex the same as provider
38+ }
39+
40+ class CredentialTestingClient : public AWSClient
41+ {
42+ public:
43+ CredentialTestingClient () : AWSClient(ClientConfiguration(), Aws::MakeShared<AWSAuthV4Signer>(TEST_LOG_TAG, Aws::MakeShared<DefaultAWSCredentialsProviderChain>(TEST_LOG_TAG),
44+ " service" , Aws::Region::US_EAST_1), Aws::MakeShared<MockAWSErrorMarshaller>(TEST_LOG_TAG)) {}
45+ CredentialTestingClient (const Aws::Client::ClientConfiguration& configuration, const std::shared_ptr<Aws::Client::AWSAuthSigner>& signer) :
46+ AWSClient (configuration, signer, Aws::MakeShared<MockAWSErrorMarshaller>(TEST_LOG_TAG)) {}
47+
48+ Aws::Client::HttpResponseOutcome PublicAttemptExhaustively (
49+ const Aws::Http::URI& uri,
50+ const Aws::AmazonWebServiceRequest& request,
51+ Http::HttpMethod method,
52+ const char * signerName) {
53+ return AttemptExhaustively (uri.GetURIString (), request, method, signerName);
54+ }
55+
56+ Aws::Client::AWSError<Aws::Client::CoreErrors> BuildAWSError (const std::shared_ptr<Aws::Http::HttpResponse>&) const override
57+ {
58+ Aws::Client::AWSError<Aws::Client::CoreErrors> error;
59+ return error;
60+ }
61+ };
62+
63+ class SSOCredentialsProviderTrackingTest : public Aws ::Testing::AwsCppSdkGTestSuite {
64+ protected:
65+ void SetUp () override {
66+ AwsCppSdkGTestSuite::SetUp ();
67+
68+ // Build paths the same way the SDK does
69+ const Aws::String profileDir = ProfileConfigFileAWSCredentialsProvider::GetProfileDirectory ();
70+ const Aws::String ssoDir = profileDir + PATH_DELIM + " sso" ;
71+ const Aws::String cacheDir = ssoDir + PATH_DELIM + " cache" ;
72+
73+ CreateDirectoryIfNotExists (profileDir.c_str ());
74+ CreateDirectoryIfNotExists (ssoDir.c_str ());
75+ CreateDirectoryIfNotExists (cacheDir.c_str ());
76+
77+ // Point AWS_CONFIG_FILE at a unique temp path the provider will read
78+ StringStream ss;
79+ ss << Aws::Auth::GetConfigProfileFilename () << " _blah" << std::this_thread::get_id ();
80+ m_configPath = ss.str ();
81+ Aws::Environment::SetEnv (" AWS_CONFIG_FILE" , m_configPath.c_str (), 1 );
82+
83+ m_profileDir = profileDir;
84+ m_ssoCacheDir = cacheDir;
85+
86+ // Mock HTTP client
87+ mockHttpClient = Aws::MakeShared<MockHttpClient>(TEST_LOG_TAG);
88+ mockHttpClientFactory = Aws::MakeShared<MockHttpClientFactory>(TEST_LOG_TAG);
89+ mockHttpClientFactory->SetClient (mockHttpClient);
90+ SetHttpClientFactory (mockHttpClientFactory);
91+ }
92+
93+ void TearDown () override {
94+ if (mockHttpClient) { mockHttpClient->Reset (); mockHttpClient = nullptr ; }
95+ mockHttpClientFactory = nullptr ;
96+ Aws::FileSystem::RemoveFileIfExists (m_configPath.c_str ());
97+ AwsCppSdkGTestSuite::TearDown ();
98+ }
99+
100+ void CreateTestConfig (const Aws::String& startUrl) {
101+ Aws::OFStream cfg (m_configPath.c_str ());
102+ cfg << " [default]\n "
103+ " sso_session = my-sso\n "
104+ " sso_account_id = 123456789012\n "
105+ " sso_role_name = TestRole\n "
106+ " \n "
107+ " [sso-session my-sso]\n "
108+ " sso_region = us-east-1\n "
109+ " sso_start_url = " << startUrl << " \n " ;
110+ cfg.close ();
111+
112+ Aws::IFStream check (m_configPath.c_str ());
113+ ASSERT_TRUE (check.good ()) << " Config not created at: " << m_configPath;
114+ check.close ();
115+
116+ Aws::Config::ReloadCachedConfigFile ();
117+ }
118+
119+ void CreateSSOTokenFile (const Aws::String& startUrl) {
120+ const Aws::String hash = computeHashedStartUrl (startUrl);
121+ const Aws::String tokenPath = m_ssoCacheDir + PATH_DELIM + hash + " .json" ;
122+
123+ Aws::OFStream tokenFile (tokenPath.c_str ());
124+ ASSERT_TRUE (tokenFile.good ()) << " Failed to open " << tokenPath;
125+
126+ const auto futureTime = DateTime::Now () + std::chrono::hours (1 );
127+ tokenFile << " {\n "
128+ " \" accessToken\" : \" test-token\" ,\n "
129+ " \" expiresAt\" : \" " << futureTime.ToGmtString (DateFormat::ISO_8601) << " \" ,\n "
130+ " \" region\" : \" us-east-1\" ,\n "
131+ " \" startUrl\" : \" " << startUrl << " \"\n "
132+ " }\n " ;
133+ tokenFile.close ();
134+
135+ Aws::IFStream check (tokenPath.c_str ());
136+ ASSERT_TRUE (check.good ()) << " Token not created at: " << tokenPath;
137+ check.close ();
138+ }
139+
140+ void CreateSSOSessionTokenFile (const Aws::String& sessionName) {
141+ const Aws::String hash = Aws::Utils::HashingUtils::HexEncode (
142+ Aws::Utils::HashingUtils::CalculateSHA1 (sessionName));
143+ const Aws::String tokenPath = m_ssoCacheDir + PATH_DELIM + hash + " .json" ;
144+
145+ Aws::OFStream tokenFile (tokenPath.c_str ());
146+ ASSERT_TRUE (tokenFile.good ()) << " Failed to open " << tokenPath;
147+
148+ const auto futureTime = Aws::Utils::DateTime::Now ().Millis () + 3600000 ;
149+ const auto futureDateTime = Aws::Utils::DateTime (futureTime);
150+ tokenFile << " {\n "
151+ " \" accessToken\" : \" test-token\" ,\n "
152+ " \" expiresAt\" : \" " << futureDateTime.ToGmtString (Aws::Utils::DateFormat::ISO_8601) << " \"\n "
153+ // (region/startUrl fields are optional on this path)
154+ " }\n " ;
155+ tokenFile.close ();
156+
157+ Aws::IFStream check (tokenPath.c_str ());
158+ ASSERT_TRUE (check.good ()) << " Token not created at: " << tokenPath;
159+ check.close ();
160+ }
161+
162+ void RunTestWithCredentialsProvider (const std::shared_ptr<AWSCredentialsProvider>& provider, const Aws::String& marker) {
163+ // 200 OK dummy response for the signed call
164+ auto req = CreateHttpRequest (URI (" https://test-service.us-east-1.amazonaws.com/" ), HttpMethod::HTTP_POST, Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
165+ auto ok = Aws::MakeShared<StandardHttpResponse>(TEST_LOG_TAG, req);
166+ ok->SetResponseCode (HttpResponseCode::OK);
167+ ok->GetResponseBody () << " {}" ;
168+ mockHttpClient->AddResponseToReturn (ok);
169+
170+ ClientConfigurationInitValues initVals; initVals.shouldDisableIMDS = true ;
171+ ClientConfiguration cfg (initVals);
172+ cfg.region = Aws::Region::US_EAST_1;
173+
174+ auto signer = Aws::MakeShared<Aws::Client::AWSAuthV4Signer>(TEST_LOG_TAG, provider, " test-service" , cfg.region );
175+ CredentialTestingClient client (cfg, signer);
176+ AmazonWebServiceRequestMock mockReq;
177+
178+ // Use public AWS client method to make a request
179+ URI uri (" https://test-service.us-east-1.amazonaws.com/" );
180+ auto outcome = client.PublicAttemptExhaustively (uri, mockReq, HttpMethod::HTTP_POST, Aws::Auth::SIGV4_SIGNER);
181+ ASSERT_TRUE (outcome.IsSuccess ());
182+
183+ auto last = mockHttpClient->GetMostRecentHttpRequest ();
184+ ASSERT_TRUE (last.HasHeader (USER_AGENT_HEADER));
185+ const auto userAgent = last.GetHeaderValue (USER_AGENT_HEADER);
186+ ASSERT_FALSE (userAgent.empty ());
187+
188+ const auto userAgentParsed = StringUtils::Split (userAgent, ' ' );
189+ int mCount = 0 ;
190+ for (const auto & p : userAgentParsed) if (p.find (" m/" ) != Aws::String::npos) ++mCount ;
191+ EXPECT_EQ (1 , mCount ); // only one m/ section
192+
193+ auto businessMetrics = std::find_if (userAgentParsed.begin (), userAgentParsed.end (),
194+ [&marker](const Aws::String& v){ return v.find (" m/" ) != Aws::String::npos && v.find (marker) != Aws::String::npos; });
195+ EXPECT_TRUE (businessMetrics != userAgentParsed.end ());
196+ }
197+
198+ Aws::String m_profileDir;
199+ Aws::String m_ssoCacheDir;
200+ Aws::String m_configPath;
201+
202+ std::shared_ptr<MockHttpClient> mockHttpClient;
203+ std::shared_ptr<MockHttpClientFactory> mockHttpClientFactory;
204+ };
205+
206+ TEST_F (SSOCredentialsProviderTrackingTest, TestSSOCredentialsTracking){
207+ const Aws::String startUrl = " https://test.awsapps.com/start" ;
208+
209+ CreateTestConfig (startUrl);
210+ CreateSSOSessionTokenFile (" my-sso" );
211+
212+ // Prepare mock SSO GetRoleCredentials response
213+ auto ssoReq = CreateHttpRequest (
214+ URI (" https://portal.sso.us-east-1.amazonaws.com/federation/credentials" ),
215+ HttpMethod::HTTP_GET,
216+ Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
217+
218+ auto ssoResp = Aws::MakeShared<StandardHttpResponse>(TEST_LOG_TAG, ssoReq);
219+ ssoResp->SetResponseCode (HttpResponseCode::OK);
220+ ssoResp->GetResponseBody ()
221+ << R"( {"roleCredentials":{
222+ "accessKeyId":"AKIAIOSFODNN7EXAMPLE",
223+ "secretAccessKey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
224+ "sessionToken":"AQoDYXdzEJr...",
225+ "expiration":)"
226+ << (DateTime::Now ().Millis () + 3600000 ) << " }}" ;
227+ mockHttpClient->AddResponseToReturn (ssoResp);
228+
229+ // Provider should read config + token from the real cache dir and call mock
230+ auto provider = Aws::MakeShared<SSOCredentialsProvider>(TEST_LOG_TAG);
231+ auto creds = provider->GetAWSCredentials ();
232+
233+ ASSERT_FALSE (creds.IsEmpty ());
234+ EXPECT_EQ (" AKIAIOSFODNN7EXAMPLE" , creds.GetAWSAccessKeyId ());
235+ EXPECT_EQ (" wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" , creds.GetAWSSecretKey ());
236+
237+ // Fire a signed request and assert the business metric appears once
238+ RunTestWithCredentialsProvider (provider, " s" );
239+ }
240+
241+ TEST_F (SSOCredentialsProviderTrackingTest, TestSSOLegacyCredentialsTracking){
242+ const Aws::String startUrl = " https://test.awsapps.com/start" ;
243+
244+ // Create legacy SSO config (without sso_session)
245+ Aws::OFStream cfg (m_configPath.c_str ());
246+ cfg << " [default]\n "
247+ " sso_account_id = 123456789012\n "
248+ " sso_region = us-east-1\n "
249+ " sso_role_name = TestRole\n "
250+ " sso_start_url = " << startUrl << " \n " ;
251+ cfg.close ();
252+ Aws::Config::ReloadCachedConfigFile ();
253+
254+ CreateSSOTokenFile (startUrl);
255+
256+ // Prepare mock SSO GetRoleCredentials response
257+ auto ssoReq = CreateHttpRequest (
258+ URI (" https://portal.sso.us-east-1.amazonaws.com/federation/credentials" ),
259+ HttpMethod::HTTP_GET,
260+ Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
261+
262+ auto ssoResp = Aws::MakeShared<StandardHttpResponse>(TEST_LOG_TAG, ssoReq);
263+ ssoResp->SetResponseCode (HttpResponseCode::OK);
264+ ssoResp->GetResponseBody ()
265+ << R"( {"roleCredentials":{
266+ "accessKeyId":"AKIAIOSFODNN7EXAMPLE",
267+ "secretAccessKey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
268+ "sessionToken":"AQoDYXdzEJr...",
269+ "expiration":)" << (DateTime::Now ().Millis () + 3600000 ) << " }}" ;
270+ mockHttpClient->AddResponseToReturn (ssoResp);
271+
272+ auto provider = Aws::MakeShared<SSOCredentialsProvider>(TEST_LOG_TAG);
273+ auto creds = provider->GetAWSCredentials ();
274+
275+ ASSERT_FALSE (creds.IsEmpty ());
276+ EXPECT_EQ (" AKIAIOSFODNN7EXAMPLE" , creds.GetAWSAccessKeyId ());
277+ EXPECT_EQ (" wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" , creds.GetAWSSecretKey ());
278+
279+ // Fire a signed request and assert the legacy SSO business metric appears
280+ RunTestWithCredentialsProvider (provider, " u" );
281+ }
0 commit comments