Skip to content

Commit 82de639

Browse files
authored
Create separate lists of characters that are allowed within a tenant id vs scopes in AzureCliCredential. (#5086)
* Create separate lists of characters that are allowed within a tenant id vs scopes in AzureCliCredential. * Update test to catch the particular exception we expect to be thrown for tenant id but not for scopes. * Address PR feedback.
1 parent 677a1da commit 82de639

File tree

4 files changed

+159
-22
lines changed

4 files changed

+159
-22
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
### Other Changes
1414

15+
- Create separate lists of characters that are allowed within tenant ids and scopes in `AzureCliCredential`.
16+
1517
## 1.6.0-beta.3 (2023-10-12)
1618

1719
### Bugs Fixed

sdk/identity/azure-identity/inc/azure/identity/azure_cli_credential.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ namespace Azure { namespace Identity {
7474
DateTime::duration cliProcessTimeout,
7575
std::vector<std::string> additionallyAllowedTenants);
7676

77-
void ThrowIfNotSafeCmdLineInput(std::string const& input, std::string const& description) const;
77+
void ThrowIfNotSafeCmdLineInput(
78+
std::string const& input,
79+
std::string const& allowedChars,
80+
std::string const& description) const;
7881

7982
public:
8083
/**

sdk/identity/azure-identity/src/azure_cli_credential.cpp

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,20 @@ using Azure::Identity::_detail::TokenCredentialImpl;
5757

5858
void AzureCliCredential::ThrowIfNotSafeCmdLineInput(
5959
std::string const& input,
60+
std::string const& allowedChars,
6061
std::string const& description) const
6162
{
6263
for (auto const c : input)
6364
{
64-
switch (c)
65+
if (allowedChars.find(c) != std::string::npos)
6566
{
66-
case ':':
67-
case '/':
68-
case '.':
69-
case '-':
70-
case '_':
71-
break;
72-
73-
default:
74-
if (!StringExtensions::IsAlphaNumeric(c))
75-
{
76-
throw AuthenticationException(
77-
GetCredentialName() + ": Unsafe command line input found in " + description + ": "
78-
+ input);
79-
}
67+
continue;
68+
}
69+
if (!StringExtensions::IsAlphaNumeric(c))
70+
{
71+
throw AuthenticationException(
72+
GetCredentialName() + ": Unsafe command line input found in " + description + ": "
73+
+ input);
8074
}
8175
}
8276
}
@@ -119,8 +113,10 @@ AzureCliCredential::AzureCliCredential(const Core::Credentials::TokenCredentialO
119113
std::string AzureCliCredential::GetAzCommand(std::string const& scopes, std::string const& tenantId)
120114
const
121115
{
122-
ThrowIfNotSafeCmdLineInput(scopes, "Scopes");
123-
ThrowIfNotSafeCmdLineInput(m_tenantId, "TenantID");
116+
// The OAuth 2.0 RFC (https://datatracker.ietf.org/doc/html/rfc6749#section-3.3) allows space as
117+
// well for a list of scopes, but that isn't currently required.
118+
ThrowIfNotSafeCmdLineInput(scopes, ".-:/_", "Scopes");
119+
ThrowIfNotSafeCmdLineInput(m_tenantId, ".-", "TenantID");
124120
std::string command = "az account get-access-token --output json --scope \"" + scopes + "\"";
125121

126122
if (!tenantId.empty())

sdk/identity/azure-identity/test/ut/azure_cli_credential_test.cpp

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,19 +344,33 @@ TEST(AzureCliCredential, UnsafeChars)
344344
}
345345
}
346346

347-
TEST(AzureCliCredential, SpaceNotAllowed)
347+
class ParameterizedTestForDisallowedChars : public ::testing::TestWithParam<std::string> {
348+
protected:
349+
std::string value;
350+
};
351+
352+
TEST_P(ParameterizedTestForDisallowedChars, DisallowedCharsForScopeAndTenantId)
348353
{
349-
std::string const invalid = "space character";
354+
std::string const InvalidValue = GetParam();
350355

351356
{
352357
AzureCliCredentialOptions options;
353358
options.TenantId = "01234567-89AB-CDEF-0123-456789ABCDEF";
354-
options.TenantId += invalid;
359+
options.TenantId += InvalidValue;
355360
AzureCliCredential azCliCred(options);
356361

357362
TokenRequestContext trc;
358363
trc.Scopes.push_back(std::string("https://storage.azure.com/.default"));
359364
EXPECT_THROW(static_cast<void>(azCliCred.GetToken(trc, {})), AuthenticationException);
365+
366+
try
367+
{
368+
auto const token = azCliCred.GetToken(trc, {});
369+
}
370+
catch (AuthenticationException const& e)
371+
{
372+
EXPECT_TRUE(std::string(e.what()).find("Unsafe") != std::string::npos) << e.what();
373+
}
360374
}
361375

362376
{
@@ -365,12 +379,134 @@ TEST(AzureCliCredential, SpaceNotAllowed)
365379
AzureCliCredential azCliCred(options);
366380

367381
TokenRequestContext trc;
368-
trc.Scopes.push_back(std::string("https://storage.azure.com/.default") + invalid);
382+
trc.Scopes.push_back(std::string("https://storage.azure.com/.default") + InvalidValue);
383+
EXPECT_THROW(static_cast<void>(azCliCred.GetToken(trc, {})), AuthenticationException);
384+
385+
try
386+
{
387+
auto const token = azCliCred.GetToken(trc, {});
388+
}
389+
catch (AuthenticationException const& e)
390+
{
391+
EXPECT_TRUE(std::string(e.what()).find("Unsafe") != std::string::npos) << e.what();
392+
}
393+
}
394+
}
395+
396+
INSTANTIATE_TEST_SUITE_P(
397+
AzureCliCredential,
398+
ParameterizedTestForDisallowedChars,
399+
::testing::Values(" ", "|", "`", "\"", "'", ";", "&"));
400+
401+
class ParameterizedTestForCharDifferences : public ::testing::TestWithParam<std::string> {
402+
protected:
403+
std::string value;
404+
};
405+
406+
TEST_P(ParameterizedTestForCharDifferences, ValidCharsForScopeButNotTenantId)
407+
{
408+
std::string const ValidScopeButNotTenantId = GetParam();
409+
410+
{
411+
AzureCliCredentialOptions options;
412+
options.TenantId = "01234567-89AB-CDEF-0123-456789ABCDEF";
413+
options.TenantId += ValidScopeButNotTenantId;
414+
AzureCliCredential azCliCred(options);
369415

416+
TokenRequestContext trc;
417+
trc.Scopes.push_back(std::string("https://storage.azure.com/.default"));
370418
EXPECT_THROW(static_cast<void>(azCliCred.GetToken(trc, {})), AuthenticationException);
419+
420+
try
421+
{
422+
auto const token = azCliCred.GetToken(trc, {});
423+
}
424+
catch (AuthenticationException const& e)
425+
{
426+
EXPECT_TRUE(std::string(e.what()).find("Unsafe") != std::string::npos) << e.what();
427+
}
428+
}
429+
430+
{
431+
AzureCliCredentialOptions options;
432+
options.CliProcessTimeout = std::chrono::hours(24);
433+
AzureCliCredential azCliCred(options);
434+
435+
TokenRequestContext trc;
436+
trc.Scopes.push_back(
437+
std::string("https://storage.azure.com/.default") + ValidScopeButNotTenantId);
438+
439+
// We expect the GetToken to fail, but not because of the unsafe chars.
440+
try
441+
{
442+
auto const token = azCliCred.GetToken(trc, {});
443+
}
444+
catch (AuthenticationException const& e)
445+
{
446+
EXPECT_TRUE(std::string(e.what()).find("Unsafe") == std::string::npos) << e.what();
447+
}
371448
}
372449
}
373450

451+
INSTANTIATE_TEST_SUITE_P(
452+
AzureCliCredential,
453+
ParameterizedTestForCharDifferences,
454+
::testing::Values(":", "/", "_"));
455+
456+
class ParameterizedTestForAllowedChars : public ::testing::TestWithParam<std::string> {
457+
protected:
458+
std::string value;
459+
};
460+
461+
TEST_P(ParameterizedTestForAllowedChars, ValidCharsForScopeAndTenantId)
462+
{
463+
std::string const ValidChars = GetParam();
464+
465+
{
466+
AzureCliCredentialOptions options;
467+
options.TenantId = "01234567-89AB-CDEF-0123-456789ABCDEF";
468+
options.TenantId += ValidChars;
469+
AzureCliCredential azCliCred(options);
470+
471+
TokenRequestContext trc;
472+
trc.Scopes.push_back(std::string("https://storage.azure.com/.default"));
473+
474+
// We expect the GetToken to fail, but not because of the unsafe chars.
475+
try
476+
{
477+
auto const token = azCliCred.GetToken(trc, {});
478+
}
479+
catch (AuthenticationException const& e)
480+
{
481+
EXPECT_TRUE(std::string(e.what()).find("Unsafe") == std::string::npos) << e.what();
482+
}
483+
}
484+
485+
{
486+
AzureCliCredentialOptions options;
487+
options.CliProcessTimeout = std::chrono::hours(24);
488+
AzureCliCredential azCliCred(options);
489+
490+
TokenRequestContext trc;
491+
trc.Scopes.push_back(std::string("https://storage.azure.com/.default") + ValidChars);
492+
493+
// We expect the GetToken to fail, but not because of the unsafe chars.
494+
try
495+
{
496+
auto const token = azCliCred.GetToken(trc, {});
497+
}
498+
catch (AuthenticationException const& e)
499+
{
500+
EXPECT_TRUE(std::string(e.what()).find("Unsafe") == std::string::npos) << e.what();
501+
}
502+
}
503+
}
504+
505+
INSTANTIATE_TEST_SUITE_P(
506+
AzureCliCredential,
507+
ParameterizedTestForAllowedChars,
508+
::testing::Values(".", "-", "A", "9"));
509+
374510
TEST(AzureCliCredential, StrictIso8601TimeFormat)
375511
{
376512
constexpr auto Token = "{\"accessToken\":\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\","

0 commit comments

Comments
 (0)