Skip to content

Commit 7acceed

Browse files
authored
[Identity] Support SecureString output for Pwsh cred (#36653)
The `Az.Accounts` module for PowerShell will soon start outputting token data as secure strings. This ensures that `AzurePowerShellCredential` can handle this format. Signed-off-by: Paul Van Eck <[email protected]>
1 parent 918f611 commit 7acceed

File tree

5 files changed

+52
-29
lines changed

5 files changed

+52
-29
lines changed

sdk/identity/azure-identity/CHANGELOG.md

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

1111
### Other Changes
1212

13+
- `AzurePowerShellCredential` now supports using secure strings when authenticating with PowerShell. ([#36653](https://github.com/Azure/azure-sdk-for-python/pull/36653))
14+
1315
## 1.18.0b1 (2024-07-16)
1416

1517
- Fixed the issue that `SharedTokenCacheCredential` was not picklable.

sdk/identity/azure-identity/azure/identity/_credentials/azure_powershell.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,24 @@
3434
exit
3535
}}
3636
37-
$token = Get-AzAccessToken -ResourceUrl '{}'{}
37+
$params = @{{ 'ResourceUrl' = '{}'; 'WarningAction' = 'Ignore' }}
3838
39-
Write-Output "`nazsdk%$($token.Token)%$($token.ExpiresOn.ToUnixTimeSeconds())`n"
39+
$tenantId = '{}'
40+
if ($tenantId.Length -gt 0) {{
41+
$params['TenantId'] = $tenantId
42+
}}
43+
44+
$useSecureString = $m.Version -ge [version]'2.17.0'
45+
if ($useSecureString) {{
46+
$params['AsSecureString'] = $true
47+
}}
48+
49+
$token = Get-AzAccessToken @params
50+
$tokenValue = $token.Token
51+
if ($useSecureString) {{
52+
$tokenValue = $tokenValue | ConvertFrom-SecureString -AsPlainText
53+
}}
54+
Write-Output "`nazsdk%$($tokenValue)%$($token.ExpiresOn.ToUnixTimeSeconds())`n"
4055
"""
4156

4257

@@ -182,10 +197,7 @@ def parse_token(output: str) -> AccessToken:
182197

183198

184199
def get_command_line(scopes: Tuple[str, ...], tenant_id: str) -> List[str]:
185-
if tenant_id:
186-
tenant_argument = " -TenantId " + tenant_id
187-
else:
188-
tenant_argument = ""
200+
tenant_argument = tenant_id if tenant_id else ""
189201
resource = _scopes_to_resource(*scopes)
190202
script = SCRIPT.format(NO_AZ_ACCOUNT_MODULE, resource, tenant_argument)
191203
encoded_script = base64.b64encode(script.encode("utf-16-le")).decode()
@@ -212,4 +224,6 @@ def raise_for_error(return_code: int, stdout: str, stderr: str) -> None:
212224
if stderr:
213225
# stderr is too noisy to include with an exception but may be useful for debugging
214226
_LOGGER.debug('%s received an error from Azure PowerShell: "%s"', AzurePowerShellCredential.__name__, stderr)
215-
raise CredentialUnavailableError(message="Failed to invoke PowerShell")
227+
raise CredentialUnavailableError(
228+
message="Failed to invoke PowerShell. Enable debug logging for additional information."
229+
)

sdk/identity/azure-identity/tests/test_live_async.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,21 @@ async def test_default_credential(live_service_principal):
7373

7474

7575
@pytest.mark.manual
76+
@pytest.mark.asyncio
7677
async def test_cli_credential():
7778
credential = AzureCliCredential()
7879
await get_token(credential)
7980

8081

8182
@pytest.mark.manual
83+
@pytest.mark.asyncio
8284
async def test_dev_cli_credential():
8385
credential = AzureDeveloperCliCredential()
8486
await get_token(credential)
8587

8688

8789
@pytest.mark.manual
90+
@pytest.mark.asyncio
8891
async def test_powershell_credential():
8992
credential = AzurePowerShellCredential()
9093
await get_token(credential)

sdk/identity/azure-identity/tests/test_powershell_credential.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ def test_get_token(stderr):
114114
assert match, "couldn't find encoded script in command line"
115115
encoded_script = match.groups()[0]
116116
decoded_script = base64.b64decode(encoded_script).decode("utf-16-le")
117-
assert "TenantId" not in decoded_script
118-
assert "Get-AzAccessToken -ResourceUrl '{}'".format(scope) in decoded_script
117+
assert "tenantId = ''" in decoded_script
118+
assert f"'ResourceUrl' = '{scope}'" in decoded_script
119119

120120
assert Popen().communicate.call_count == 1
121121
args, kwargs = Popen().communicate.call_args
@@ -292,7 +292,7 @@ def Popen(args, **kwargs):
292292

293293
def test_multitenant_authentication():
294294
first_token = "***"
295-
second_tenant = "second-tenant"
295+
second_tenant = "12345"
296296
second_token = first_token * 2
297297

298298
def fake_Popen(command, **_):
@@ -301,11 +301,12 @@ def fake_Popen(command, **_):
301301
assert match, "couldn't find encoded script in command line"
302302
encoded_script = match.groups()[0]
303303
decoded_script = base64.b64decode(encoded_script).decode("utf-16-le")
304-
match = re.search(r"Get-AzAccessToken -ResourceUrl '(\S+)'(?: -TenantId (\S+))?", decoded_script)
305-
tenant = match.groups()[1]
304+
match = re.search(r"\$tenantId\s*=\s*'([^']*)'", decoded_script)
305+
assert match
306+
tenant = match.group(1)
306307

307-
assert tenant is None or tenant == second_tenant, 'unexpected tenant "{}"'.format(tenant)
308-
token = first_token if tenant is None else second_token
308+
assert not tenant or tenant == second_tenant, 'unexpected tenant "{}"'.format(tenant)
309+
token = first_token if not tenant else second_token
309310
stdout = "azsdk%{}%{}".format(token, int(time.time()) + 3600)
310311

311312
communicate = Mock(return_value=(stdout, ""))
@@ -333,10 +334,11 @@ def fake_Popen(command, **_):
333334
assert match, "couldn't find encoded script in command line"
334335
encoded_script = match.groups()[0]
335336
decoded_script = base64.b64decode(encoded_script).decode("utf-16-le")
336-
match = re.search(r"Get-AzAccessToken -ResourceUrl '(\S+)'(?: -TenantId (\S+))?", decoded_script)
337-
tenant = match.groups()[1]
337+
match = re.search(r"\$tenantId\s*=\s*'([^']*)'", decoded_script)
338+
assert match
339+
tenant = match.group(1)
338340

339-
assert tenant is None, "credential shouldn't accept an explicit tenant ID"
341+
assert not tenant, "credential shouldn't accept an explicit tenant ID"
340342
stdout = "azsdk%{}%{}".format(expected_token, int(time.time()) + 3600)
341343

342344
communicate = Mock(return_value=(stdout, ""))
@@ -348,5 +350,5 @@ def fake_Popen(command, **_):
348350
assert token.token == expected_token
349351

350352
with patch.dict("os.environ", {EnvironmentVariables.AZURE_IDENTITY_DISABLE_MULTITENANTAUTH: "true"}):
351-
token = credential.get_token("scope", tenant_id="some-tenant")
353+
token = credential.get_token("scope", tenant_id="12345")
352354
assert token.token == expected_token

sdk/identity/azure-identity/tests/test_powershell_credential_async.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ async def test_get_token(stderr):
104104
assert match, "couldn't find encoded script in command line"
105105
encoded_script = match.groups()[0]
106106
decoded_script = base64.b64decode(encoded_script).decode("utf-16-le")
107-
assert "TenantId" not in decoded_script
108-
assert "Get-AzAccessToken -ResourceUrl '{}'".format(scope) in decoded_script
107+
assert "tenantId = ''" in decoded_script
108+
assert f"'ResourceUrl' = '{scope}'" in decoded_script
109109

110110
assert mock_exec().result().communicate.call_count == 1
111111

@@ -289,7 +289,7 @@ async def mock_exec(*args, **kwargs):
289289

290290
async def test_multitenant_authentication():
291291
first_token = "***"
292-
second_tenant = "second-tenant"
292+
second_tenant = "12345"
293293
second_token = first_token * 2
294294

295295
async def fake_exec(*args, **_):
@@ -299,11 +299,12 @@ async def fake_exec(*args, **_):
299299
assert match, "couldn't find encoded script in command line"
300300
encoded_script = match.groups()[0]
301301
decoded_script = base64.b64decode(encoded_script).decode("utf-16-le")
302-
match = re.search(r"Get-AzAccessToken -ResourceUrl '(\S+)'(?: -TenantId (\S+))?", decoded_script)
303-
tenant = match[2]
302+
match = re.search(r"\$tenantId\s*=\s*'([^']*)'", decoded_script)
303+
assert match
304+
tenant = match.group(1)
304305

305-
assert tenant is None or tenant == second_tenant, 'unexpected tenant "{}"'.format(tenant)
306-
token = first_token if tenant is None else second_token
306+
assert not tenant or tenant == second_tenant, 'unexpected tenant "{}"'.format(tenant)
307+
token = first_token if not tenant else second_token
307308
stdout = "azsdk%{}%{}".format(token, int(time.time()) + 3600)
308309

309310
communicate = Mock(return_value=get_completed_future((stdout.encode(), b"")))
@@ -332,10 +333,11 @@ async def fake_exec(*args, **_):
332333
assert match, "couldn't find encoded script in command line"
333334
encoded_script = match.groups()[0]
334335
decoded_script = base64.b64decode(encoded_script).decode("utf-16-le")
335-
match = re.search(r"Get-AzAccessToken -ResourceUrl '(\S+)'(?: -TenantId (\S+))?", decoded_script)
336-
tenant = match[2]
336+
match = re.search(r"\$tenantId\s*=\s*'([^']*)'", decoded_script)
337+
assert match
338+
tenant = match.group(1)
337339

338-
assert tenant is None, "credential shouldn't accept an explicit tenant ID"
340+
assert not tenant, "credential shouldn't accept an explicit tenant ID"
339341
stdout = "azsdk%{}%{}".format(expected_token, int(time.time()) + 3600)
340342
communicate = Mock(return_value=get_completed_future((stdout.encode(), b"")))
341343
return Mock(communicate=communicate, returncode=0)
@@ -346,5 +348,5 @@ async def fake_exec(*args, **_):
346348
assert token.token == expected_token
347349

348350
with patch.dict("os.environ", {EnvironmentVariables.AZURE_IDENTITY_DISABLE_MULTITENANTAUTH: "true"}):
349-
token = await credential.get_token("scope", tenant_id="some-tenant")
351+
token = await credential.get_token("scope", tenant_id="12345")
350352
assert token.token == expected_token

0 commit comments

Comments
 (0)