// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "azure/identity/environment_credential.hpp" #include "credential_test_helper.hpp" #include #include using Azure::Core::Credentials::TokenCredentialOptions; using Azure::Core::Http::HttpMethod; using Azure::Identity::EnvironmentCredential; using Azure::Identity::Test::_detail::CredentialTestHelper; TEST(EnvironmentCredential, GetCredentialName) { CredentialTestHelper::EnvironmentOverride const env({ {"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, {"AZURE_AUTHORITY_HOST", ""}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, }); EnvironmentCredential const cred; EXPECT_EQ(cred.GetCredentialName(), "EnvironmentCredential"); } TEST(EnvironmentCredential, RegularClientSecretCredential) { using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Verbose); Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); auto const actual = CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); EXPECT_EQ(log[0].first, Logger::Level::Informational); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential gets created with ClientSecretCredential."); EXPECT_EQ(log[1].first, Logger::Level::Verbose); EXPECT_EQ( log[1].second, "Identity: EnvironmentCredential: 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', " "'AZURE_CLIENT_SECRET', and 'AZURE_AUTHORITY_HOST' environment variables are set, so " "ClientSecretCredential with corresponding tenantId, clientId, clientSecret, and " "authorityHost gets created."); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); EXPECT_EQ(actual.Requests.size(), 1U); EXPECT_EQ(actual.Responses.size(), 1U); auto const& request = actual.Requests.at(0); auto const& response = actual.Responses.at(0); EXPECT_EQ(request.HttpMethod, HttpMethod::Post); EXPECT_EQ( request.AbsoluteUrl, "https://microsoft.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); { constexpr char expectedBody[] = "grant_type=client_credentials" "&client_id=fedcba98-7654-3210-0123-456789abcdef" "&client_secret=CLIENTSECRET" "&scope=https%3A%2F%2Fazure.com%2F.default"; // cspell:disable-line EXPECT_EQ(request.Body, expectedBody); EXPECT_NE(request.Headers.find("Content-Length"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); } EXPECT_NE(request.Headers.find("Content-Type"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); EXPECT_EQ(response.AccessToken.Token, "ACCESSTOKEN1"); using namespace std::chrono_literals; EXPECT_GE(response.AccessToken.ExpiresOn, response.EarliestExpiration + 3600s); EXPECT_LE(response.AccessToken.ExpiresOn, response.LatestExpiration + 3600s); Logger::SetListener(nullptr); } TEST(EnvironmentCredential, AzureStackClientSecretCredential) { using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Informational); // Setting it to Informational instead of Verbose // will result in less detailed log message. Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); auto const actual = CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", "adfs"}, {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); EXPECT_EQ(log[0].first, Logger::Level::Informational); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential gets created with ClientSecretCredential."); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); EXPECT_EQ(actual.Requests.size(), 1U); EXPECT_EQ(actual.Responses.size(), 1U); auto const& request = actual.Requests.at(0); auto const& response = actual.Responses.at(0); EXPECT_EQ(request.AbsoluteUrl, "https://microsoft.com/adfs/oauth2/token"); { constexpr char expectedBody[] = "grant_type=client_credentials" "&client_id=fedcba98-7654-3210-0123-456789abcdef" "&client_secret=CLIENTSECRET" "&scope=https%3A%2F%2Fazure.com"; // cspell:disable-line EXPECT_EQ(request.Body, expectedBody); EXPECT_NE(request.Headers.find("Content-Length"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); } EXPECT_NE(request.Headers.find("Content-Type"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); EXPECT_NE(request.Headers.find("Host"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Host"), "microsoft.com"); EXPECT_EQ(response.AccessToken.Token, "ACCESSTOKEN1"); using namespace std::chrono_literals; EXPECT_GE(response.AccessToken.ExpiresOn, response.EarliestExpiration + 3600s); EXPECT_LE(response.AccessToken.ExpiresOn, response.LatestExpiration + 3600s); Logger::SetListener(nullptr); } TEST(EnvironmentCredential, Unavailable) { using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Verbose); Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); static_cast(CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", ""}, {"AZURE_CLIENT_ID", ""}, {"AZURE_CLIENT_SECRET", ""}, {"AZURE_AUTHORITY_HOST", ""}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential was not initialized with underlying credential."); EXPECT_EQ(log[1].first, Logger::Level::Verbose); EXPECT_EQ( log[1].second, "Identity: EnvironmentCredential: Both 'AZURE_TENANT_ID' and 'AZURE_CLIENT_ID'," " and at least one of 'AZURE_CLIENT_SECRET', 'AZURE_CLIENT_CERTIFICATE_PATH'" " needs to be set. Additionally, 'AZURE_AUTHORITY_HOST' could be set" " to override the default authority host. Currently:\n" " * 'AZURE_TENANT_ID' is NOT set\n" " * 'AZURE_CLIENT_ID' is NOT set\n" " * 'AZURE_CLIENT_SECRET' is NOT set\n" " * 'AZURE_CLIENT_CERTIFICATE_PATH' is NOT set\n" " * 'AZURE_AUTHORITY_HOST' is NOT set\n"); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential authentication unavailable. " "See earlier EnvironmentCredential log messages for details."); log.clear(); return token; })); Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretDefaultAuthority) { using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Verbose); Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); auto const actual = CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, {"AZURE_AUTHORITY_HOST", ""}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); EXPECT_EQ(log[0].first, Logger::Level::Informational); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential gets created with ClientSecretCredential."); EXPECT_EQ(log[1].first, Logger::Level::Verbose); EXPECT_EQ( log[1].second, "Identity: EnvironmentCredential: 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', and " "'AZURE_CLIENT_SECRET' environment variables are set, so ClientSecretCredential with " "corresponding tenantId, clientId, and clientSecret gets created."); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); EXPECT_EQ(actual.Requests.size(), 1U); EXPECT_EQ(actual.Responses.size(), 1U); auto const& request = actual.Requests.at(0); auto const& response = actual.Responses.at(0); EXPECT_EQ(request.HttpMethod, HttpMethod::Post); EXPECT_EQ( request.AbsoluteUrl, "https://login.microsoftonline.com/01234567-89ab-cdef-fedc-ba8976543210/oauth2/v2.0/token"); { constexpr char expectedBody[] = "grant_type=client_credentials" "&client_id=fedcba98-7654-3210-0123-456789abcdef" "&client_secret=CLIENTSECRET" "&scope=https%3A%2F%2Fazure.com%2F.default"; // cspell:disable-line EXPECT_EQ(request.Body, expectedBody); EXPECT_NE(request.Headers.find("Content-Length"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Content-Length"), std::to_string(sizeof(expectedBody) - 1)); } EXPECT_NE(request.Headers.find("Content-Type"), request.Headers.end()); EXPECT_EQ(request.Headers.at("Content-Type"), "application/x-www-form-urlencoded"); EXPECT_EQ(response.AccessToken.Token, "ACCESSTOKEN1"); using namespace std::chrono_literals; EXPECT_GE(response.AccessToken.ExpiresOn, response.EarliestExpiration + 3600s); EXPECT_LE(response.AccessToken.ExpiresOn, response.LatestExpiration + 3600s); Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretNoTenantId) { using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Verbose); Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); static_cast(CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", ""}, {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential was not initialized with underlying credential."); EXPECT_EQ(log[1].first, Logger::Level::Verbose); EXPECT_EQ( log[1].second, "Identity: EnvironmentCredential: Both 'AZURE_TENANT_ID' and 'AZURE_CLIENT_ID'," " and at least one of 'AZURE_CLIENT_SECRET', 'AZURE_CLIENT_CERTIFICATE_PATH'" " needs to be set. Additionally, 'AZURE_AUTHORITY_HOST' could be set" " to override the default authority host. Currently:\n" " * 'AZURE_TENANT_ID' is NOT set\n" " * 'AZURE_CLIENT_ID' is set\n" " * 'AZURE_CLIENT_SECRET' is set\n" " * 'AZURE_CLIENT_CERTIFICATE_PATH' is NOT set\n" " * 'AZURE_AUTHORITY_HOST' is set\n"); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential authentication unavailable. " "See earlier EnvironmentCredential log messages for details."); log.clear(); return token; })); Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretNoClientId) { using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Warning); // Setting it to Warning instead of Verbose will result // in less detailed Warning message. Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); static_cast(CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, {"AZURE_CLIENT_ID", ""}, {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential was not initialized with underlying credential."); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential authentication unavailable. " "See earlier EnvironmentCredential log messages for details."); log.clear(); return token; })); Logger::SetListener(nullptr); } TEST(EnvironmentCredential, ClientSecretNoClientSecret) { using Azure::Core::Credentials::AccessToken; using Azure::Core::Credentials::AuthenticationException; using Azure::Core::Diagnostics::Logger; using LogMsgVec = std::vector>; LogMsgVec log; Logger::SetLevel(Logger::Level::Verbose); Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); static_cast(CredentialTestHelper::SimulateTokenRequest( [&](auto transport) { TokenCredentialOptions options; options.Transport.Transport = transport; CredentialTestHelper::EnvironmentOverride const env( {{"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, {"AZURE_CLIENT_SECRET", ""}, {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, {"AZURE_USERNAME", ""}, {"AZURE_PASSWORD", ""}, {"AZURE_CLIENT_CERTIFICATE_PATH", ""}}); auto credential = std::make_unique(options); EXPECT_EQ(log.size(), LogMsgVec::size_type(2)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential was not initialized with underlying credential."); EXPECT_EQ(log[1].first, Logger::Level::Verbose); EXPECT_EQ( log[1].second, "Identity: EnvironmentCredential: Both 'AZURE_TENANT_ID' and 'AZURE_CLIENT_ID'," " and at least one of 'AZURE_CLIENT_SECRET', 'AZURE_CLIENT_CERTIFICATE_PATH'" " needs to be set. Additionally, 'AZURE_AUTHORITY_HOST' could be set" " to override the default authority host. Currently:\n" " * 'AZURE_TENANT_ID' is set\n" " * 'AZURE_CLIENT_ID' is set\n" " * 'AZURE_CLIENT_SECRET' is NOT set\n" " * 'AZURE_CLIENT_CERTIFICATE_PATH' is NOT set\n" " * 'AZURE_AUTHORITY_HOST' is set\n"); log.clear(); return credential; }, {{"https://azure.com/.default"}}, {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}, [&](auto& credential, auto& tokenRequestContext, auto& context) { AccessToken token; EXPECT_THROW( token = credential.GetToken(tokenRequestContext, context), AuthenticationException); EXPECT_EQ(log.size(), LogMsgVec::size_type(1)); EXPECT_EQ(log[0].first, Logger::Level::Warning); EXPECT_EQ( log[0].second, "Identity: EnvironmentCredential authentication unavailable. " "See earlier EnvironmentCredential log messages for details."); log.clear(); return token; })); Logger::SetListener(nullptr); }