Skip to content

Commit 0a76f9c

Browse files
authored
[Identity] Prepare patch 1.14.1 release (#50847)
* [Identity] Update AzurePowerShellCredential to handle Get-AzAccessToken breaking change (#50654) * handle new format * Update changelog * Add new test * Applied copilot suggestion * Use different logic for WindowsPowershell * Simplify logic and follow the migration guide pattern * Update test after logic change in PowerShell script * Prepare release script * Patch instead of minor version increase * Fix API compat version after mistakenly using version 1.15.0 when running the PrepareRelease script * fix changelog
1 parent 7882e55 commit 0a76f9c

File tree

5 files changed

+61
-7
lines changed

5 files changed

+61
-7
lines changed

sdk/identity/Azure.Identity/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release History
22

3+
## 1.14.1 (2025-07-08)
4+
5+
### Bugs Fixed
6+
7+
- Added support in AzurePowerShellCredential for the Az.Accounts 5.0.0+ (Az 14.0.0+) breaking change where Get-AzAccessToken returns PSSecureAccessToken with a SecureString Token property instead of plaintext.
8+
39
## 1.14.0 (2025-05-13)
410

511
### Other Changes

sdk/identity/Azure.Identity/src/Azure.Identity.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
<PropertyGroup>
33
<Description>Provides APIs for authenticating to Microsoft Entra ID</Description>
44
<AssemblyTitle>Microsoft Azure.Identity Component</AssemblyTitle>
5-
<Version>1.14.0</Version>
5+
<Version>1.14.1</Version>
66
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
7-
<ApiCompatVersion>1.13.2</ApiCompatVersion>
7+
<ApiCompatVersion>1.14.0</ApiCompatVersion>
88
<PackageTags>Microsoft Azure Identity;$(PackageCommonTags)</PackageTags>
99
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
1010
<NoWarn>$(NoWarn);3021;AZC0011</NoWarn>

sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ private static void CheckForErrors(string output, int exitCode)
210210

211211
private static void ValidateResult(string output)
212212
{
213-
if (output.IndexOf(@"<Property Name=""Token"" Type=""System.String"">", StringComparison.OrdinalIgnoreCase) < 0)
213+
// Check for Token property in the XML output, regardless of the property type, to handle both legacy and new secure string format.
214+
if (output.IndexOf(@"<Property Name=""Token""", StringComparison.OrdinalIgnoreCase) < 0)
214215
{
215216
throw new CredentialUnavailableException("PowerShell did not return a valid response.");
216217
}
@@ -261,16 +262,24 @@ private void GetFileNameAndArguments(string resource, string tenantId, out strin
261262
$params['TenantId'] = '{tenantId}'
262263
}}
263264
264-
$useSecureString = $m.Version -ge [version]'2.17.0'
265-
if ($useSecureString) {{
265+
# For Az.Accounts 2.17.0+ but below 5.0.0, explicitly request secure string
266+
if ($m.Version -ge [version]'2.17.0' -and $m.Version -lt [version]'5.0.0') {{
266267
$params['AsSecureString'] = $true
267268
}}
268269
269270
$token = Get-AzAccessToken @params
270271
271272
$customToken = New-Object -TypeName psobject
272-
if ($useSecureString) {{
273-
$customToken | Add-Member -MemberType NoteProperty -Name Token -Value (ConvertFrom-SecureString -AsPlainText $token.Token)
273+
274+
# If the token is a SecureString, convert to plain text using recommended pattern
275+
if ($token.Token -is [System.Security.SecureString]) {{
276+
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.Token)
277+
try {{
278+
$plainToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)
279+
}} finally {{
280+
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
281+
}}
282+
$customToken | Add-Member -MemberType NoteProperty -Name Token -Value $plainToken
274283
}} else {{
275284
$customToken | Add-Member -MemberType NoteProperty -Name Token -Value $token.Token
276285
}}

sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,5 +331,35 @@ public void VerifyGetTokenScopeValidation(char testChar)
331331
Assert.ThrowsAsync<AuthenticationFailedException>(async () => await credential.GetTokenAsync(tokenRequestContext), ScopeUtilities.InvalidScopeMessage);
332332
}
333333
}
334+
335+
[Test]
336+
public async Task AuthenticateWithAzurePowerShellCredential_HandlesSecureStringToken()
337+
{
338+
var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzurePowerShellSecureString(TimeSpan.FromSeconds(30));
339+
var testProcess = new TestProcess { Output = processOutput };
340+
AzurePowerShellCredential credential = InstrumentClient(
341+
new AzurePowerShellCredential(
342+
new AzurePowerShellCredentialOptions(),
343+
CredentialPipeline.GetInstance(null),
344+
new TestProcessService(testProcess, true)));
345+
346+
AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
347+
348+
Assert.AreEqual(expectedToken, actualToken.Token);
349+
Assert.AreEqual(expectedExpiresOn, actualToken.ExpiresOn);
350+
351+
// Verify PowerShell script checks for and handles Az.Accounts module version 5.0.0+
352+
var match = System.Text.RegularExpressions.Regex.Match(testProcess.StartInfo.Arguments, "EncodedCommand\\s*\"([^\"]+)\"");
353+
if (!match.Success)
354+
{
355+
throw new InvalidOperationException("Failed to extract the encoded command from the arguments.");
356+
}
357+
var commandString = match.Groups[1].Value;
358+
var b = Convert.FromBase64String(commandString);
359+
commandString = Encoding.Unicode.GetString(b);
360+
361+
Assert.That(commandString, Does.Contain("if ($token.Token -is [System.Security.SecureString])"));
362+
Assert.That(commandString, Does.Contain("[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.Token)"));
363+
}
334364
}
335365
}

sdk/identity/Azure.Identity/tests/CredentialTestHelpers.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenF
7979
return (token, expiresOn, xml);
8080
}
8181

82+
public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenForAzurePowerShellSecureString(TimeSpan expiresOffset)
83+
{
84+
var expiresOn = DateTimeOffset.FromUnixTimeSeconds(DateTimeOffset.UtcNow.Add(expiresOffset).ToUnixTimeSeconds());
85+
var token = TokenGenerator.GenerateToken(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), expiresOn.UtcDateTime);
86+
// Simulate Az 14.0+ output with secure string token as returned by our PowerShell script after conversion
87+
var xml = @$"<Object Type=""System.Management.Automation.PSCustomObject""><Property Name=""Token"" Type=""System.String"">{token}</Property><Property Name=""ExpiresOn"" Type=""System.Int64"">{expiresOn.UtcDateTime.Ticks}</Property></Object>";
88+
return (token, expiresOn, xml);
89+
}
90+
8291
public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenForVisualStudio() => CreateTokenForVisualStudio(TimeSpan.FromSeconds(30));
8392

8493
public static (string Token, DateTimeOffset ExpiresOn, string Json) CreateTokenForVisualStudio(TimeSpan expiresOffset)

0 commit comments

Comments
 (0)