Skip to content

Commit 93bb4b7

Browse files
authored
Light up CAE support for ManagedIdentityCredential (Azure#48549)
1 parent 4ff5a17 commit 93bb4b7

File tree

5 files changed

+76
-13
lines changed

5 files changed

+76
-13
lines changed

eng/Packages.Data.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,9 @@
168168
<!-- Other approved packages -->
169169
<PackageReference Update="Microsoft.Azure.Amqp" Version="2.6.9" />
170170
<PackageReference Update="Microsoft.Azure.WebPubSub.Common" Version="1.4.0" />
171-
<PackageReference Update="Microsoft.Identity.Client" Version="4.67.2" />
172-
<PackageReference Update="Microsoft.Identity.Client.Extensions.Msal" Version="4.67.2" />
173-
<PackageReference Update="Microsoft.Identity.Client.Broker" Version="4.67.2" />
171+
<PackageReference Update="Microsoft.Identity.Client" Version="4.69.1" />
172+
<PackageReference Update="Microsoft.Identity.Client.Extensions.Msal" Version="4.69.1" />
173+
<PackageReference Update="Microsoft.Identity.Client.Broker" Version="4.69.1" />
174174

175175
<!-- TODO: Make sure this package is arch-board approved -->
176176
<PackageReference Update="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.35.0" />

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
- `VisualStudioCredential` will now correctly fall through to the next credential in the chain when no account is found by Visual Studio. ([#48464](https://github.com/Azure/azure-sdk-for-net/issues/48464))
1111

1212
### Other Changes
13+
- Updated Microsoft.Identity.Client dependency to version 4.69.1.
14+
- ManagedIdentityCredential now properly supports CAE.
1315
- An event is now logged when the `ManagedIdentityCredential` is used directly or indirectly via a credential chain indicating which managed identity source was selected and which `ManagedIdentityId` was specified.
1416
- Marked `UsernamePasswordCredential` as obsolete because Resource Owner Password Credentials (ROPC) token grant flow is incompatible with multifactor authentication (MFA), which Microsoft Entra ID requires for all tenants. See https://aka.ms/azsdk/identity/mfa for details about MFA enforcement and migration guidance.
1517

sdk/identity/Azure.Identity/src/MsalManagedIdentityClient.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ namespace Azure.Identity
1515
internal class MsalManagedIdentityClient
1616
{
1717
private readonly AsyncLockWithValue<IManagedIdentityApplication> _clientAsyncLock;
18+
private readonly AsyncLockWithValue<IManagedIdentityApplication> _clientCaeAsyncLock;
1819
private bool _isForceRefreshEnabled { get; }
1920

2021
internal bool IsSupportLoggingEnabled { get; }
2122
internal Microsoft.Identity.Client.AppConfig.ManagedIdentityId ManagedIdentityId { get; }
2223
internal bool DisableInstanceDiscovery { get; }
2324
internal CredentialPipeline Pipeline { get; }
2425
internal Uri AuthorityHost { get; }
26+
protected string[] cp1Capabilities = ["CP1"];
2527

2628
protected MsalManagedIdentityClient()
2729
{ }
@@ -49,34 +51,45 @@ public MsalManagedIdentityClient(ManagedIdentityClientOptions clientOptions)
4951

5052
Pipeline = clientOptions.Pipeline;
5153
_clientAsyncLock = new AsyncLockWithValue<IManagedIdentityApplication>();
54+
_clientCaeAsyncLock = new AsyncLockWithValue<IManagedIdentityApplication>();
5255
_isForceRefreshEnabled = clientOptions.IsForceRefreshEnabled;
5356
}
5457

55-
protected ValueTask<IManagedIdentityApplication> CreateClientAsync(bool async, CancellationToken cancellationToken)
58+
protected ValueTask<IManagedIdentityApplication> CreateClientAsync(bool async, bool enableCae, CancellationToken cancellationToken)
5659
{
57-
return CreateClientCoreAsync(async, cancellationToken);
60+
return CreateClientCoreAsync(async, enableCae, cancellationToken);
5861
}
5962

60-
protected virtual ValueTask<IManagedIdentityApplication> CreateClientCoreAsync(bool async, CancellationToken cancellationToken)
63+
protected virtual ValueTask<IManagedIdentityApplication> CreateClientCoreAsync(bool async, bool enableCae, CancellationToken cancellationToken)
6164
{
65+
string[] clientCapabilities =
66+
enableCae ? cp1Capabilities : Array.Empty<string>();
67+
6268
ManagedIdentityApplicationBuilder miAppBuilder = ManagedIdentityApplicationBuilder
6369
.Create(ManagedIdentityId)
6470
.WithHttpClientFactory(new HttpPipelineClientFactory(Pipeline.HttpPipeline), false)
6571
.WithLogging(AzureIdentityEventSource.Singleton, enablePiiLogging: IsSupportLoggingEnabled);
6672

73+
if (clientCapabilities.Length > 0)
74+
{
75+
miAppBuilder.WithClientCapabilities(clientCapabilities);
76+
}
77+
6778
return new ValueTask<IManagedIdentityApplication>(miAppBuilder.Build());
6879
}
6980

70-
protected async ValueTask<IManagedIdentityApplication> GetClientAsync(bool async, CancellationToken cancellationToken)
81+
protected async ValueTask<IManagedIdentityApplication> GetClientAsync(bool async, bool enableCae, CancellationToken cancellationToken)
7182
{
72-
using var asyncLock = await _clientAsyncLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false);
83+
using var asyncLock = enableCae ?
84+
await _clientCaeAsyncLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false) :
85+
await _clientAsyncLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false);
7386

7487
if (asyncLock.HasValue)
7588
{
7689
return asyncLock.Value;
7790
}
7891

79-
var client = await CreateClientAsync(async, cancellationToken).ConfigureAwait(false);
92+
var client = await CreateClientAsync(async, enableCae, cancellationToken).ConfigureAwait(false);
8093
asyncLock.SetValue(client);
8194
return client;
8295
}
@@ -89,10 +102,15 @@ public virtual AuthenticationResult AcquireTokenForManagedIdentity(TokenRequestC
89102

90103
public virtual async ValueTask<AuthenticationResult> AcquireTokenForManagedIdentityAsyncCore(bool async, TokenRequestContext requestContext, CancellationToken cancellationToken)
91104
{
92-
IManagedIdentityApplication client = await GetClientAsync(async, cancellationToken).ConfigureAwait(false);
105+
IManagedIdentityApplication client = await GetClientAsync(async, requestContext.IsCaeEnabled, cancellationToken).ConfigureAwait(false);
93106

94107
var builder = client.AcquireTokenForManagedIdentity(requestContext.Scopes.FirstOrDefault());
95108

109+
if (!string.IsNullOrEmpty(requestContext.Claims))
110+
{
111+
builder.WithClaims(requestContext.Claims);
112+
}
113+
96114
if (_isForceRefreshEnabled)
97115
{
98116
builder.WithForceRefresh(true);

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,16 @@ public async Task VerifyIMDSRequestWithPodIdentityEnvVarResourceIdMockAsync()
551551
var mockTransport = new MockTransport(response);
552552
var options = new TokenCredentialOptions() { Transport = mockTransport };
553553

554-
ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(new ResourceIdentifier(_expectedResourceId), options));
554+
ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(
555+
new ManagedIdentityClient(
556+
new ManagedIdentityClientOptions()
557+
{
558+
Pipeline = CredentialPipeline.GetInstance(options),
559+
ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(_expectedResourceId)),
560+
IsForceRefreshEnabled = true,
561+
Options = options
562+
})
563+
));
555564

556565
AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
557566

@@ -1182,6 +1191,40 @@ public async Task VerifyManagedIdentityIdIsLogged(ManagedIdentityId managedIdent
11821191
Assert.That(messages, Does.Contain(expectedMessage));
11831192
}
11841193

1194+
[NonParallelizable]
1195+
[Test]
1196+
public async Task VerifyImdsRequestWithCAEMockAsync()
1197+
{
1198+
string caeClaims = """{"access_token":{"nbf":{"essential":true, "value":"1724337680"}}}""";
1199+
using var environment = new TestEnvVar(new() { { "MSI_ENDPOINT", null }, { "MSI_SECRET", null }, { "IDENTITY_ENDPOINT", null }, { "IDENTITY_HEADER", null }, { "AZURE_POD_IDENTITY_AUTHORITY_HOST", null } });
1200+
1201+
var mockTransport = new MockTransport(_ => CreateMockResponse(200, Guid.NewGuid().ToString()));
1202+
var options = new TokenCredentialOptions { Transport = mockTransport };
1203+
1204+
ManagedIdentityCredential credential = InstrumentClient(new ManagedIdentityCredential(
1205+
new ManagedIdentityClient(
1206+
new ManagedIdentityClientOptions()
1207+
{
1208+
Pipeline = CredentialPipeline.GetInstance(options),
1209+
ManagedIdentityId = ManagedIdentityId.FromUserAssignedResourceId(new ResourceIdentifier(_expectedResourceId)),
1210+
IsForceRefreshEnabled = false,
1211+
Options = options
1212+
})
1213+
));
1214+
1215+
AccessToken tokenWithoutCAE1 = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
1216+
AccessToken tokenWithoutCAE2 = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
1217+
AccessToken tokenWithCAE1 = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default, isCaeEnabled: true, claims: caeClaims));
1218+
AccessToken tokenWithoutCAE3 = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default));
1219+
1220+
// TokenWithoutCAE1 and TokenWithoutCAE2 should be the same since subsequent calls to GetTokenAsync should return the same token if no claims were provided
1221+
Assert.AreEqual(tokenWithoutCAE2.Token, tokenWithoutCAE1.Token);
1222+
// TokenWithCAE1 and TokenWithoutCAE3 should be the same since subsequent calls to GetTokenAsync should return the same token if no claims were provided.
1223+
Assert.AreEqual(tokenWithoutCAE3.Token, tokenWithCAE1.Token);
1224+
// TokenWithCAE1 and TokenWithoutCAE1 should be different since the first token was requested with claims.
1225+
Assert.AreNotEqual(tokenWithCAE1.Token, tokenWithoutCAE1.Token);
1226+
}
1227+
11851228
private static IEnumerable<TestCaseData> ResourceAndClientIds()
11861229
{
11871230
yield return new TestCaseData(new object[] { null, false });

sdk/identity/Azure.Identity/tests/Mock/MockMsalManagedIdentityClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ public MockMsalManagedIdentityClient(AuthenticationResult result)
2424
public MockMsalManagedIdentityClient(ManagedIdentityClientOptions options)
2525
: base(options) { }
2626

27-
protected override ValueTask<IManagedIdentityApplication> CreateClientCoreAsync(bool async, CancellationToken cancellationToken)
27+
protected override ValueTask<IManagedIdentityApplication> CreateClientCoreAsync(bool async, bool enableCae, CancellationToken cancellationToken)
2828
{
2929
if (ClientAppFactory == null)
3030
{
31-
return base.CreateClientCoreAsync(async, cancellationToken);
31+
return base.CreateClientCoreAsync(async, enableCae, cancellationToken);
3232
}
3333

3434
return new ValueTask<IManagedIdentityApplication>(ClientAppFactory(cancellationToken));

0 commit comments

Comments
 (0)