Skip to content

Commit 16994fe

Browse files
authored
[Extensions] Support for federated managed identity (Azure#50436)
* Implementation of federated managed identity credential support for Microsoft.Extensions.Azure.
1 parent 698b33a commit 16994fe

File tree

7 files changed

+522
-22
lines changed

7 files changed

+522
-22
lines changed

sdk/extensions/Microsoft.Extensions.Azure/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44

55
### Features Added
66

7+
- Added support for [managed identity as a federated identity credential](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center#azureidentity) in the client factory by specifying configuration item `credential` as "managedidentityasfederatedidentity" and providing the following named configuration items:
8+
9+
- `tenantId` : The tenant where the target resource was created
10+
- `clientId` : The client identifier for the application, which must be granted access on the target resource
11+
- One of [`managedIdentityClientId`, `objectId`, `resourceId`] : The user-assigned managed identity which you configured as a Federated Identity Credential (FIC)
12+
- `azureCloud`: One of the following Azure cloud environments:
13+
- `public` for Entra ID Global cloud
14+
- `usgov` for Entra ID US Government
15+
- `china` for Entra ID China operated by 21Vianet
16+
717
### Breaking Changes
818

919
### Bugs Fixed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Extensions.Azure
5+
{
6+
internal static class AzureCloud
7+
{
8+
public const string Public = "public";
9+
public const string USGov = "usgov";
10+
public const string China = "china";
11+
}
12+
}

sdk/extensions/Microsoft.Extensions.Azure/src/Internal/ClientFactory.cs

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Text;
1111
using Azure.Core;
1212
using Azure.Identity;
13+
using Microsoft.Extensions.Azure.Internal;
1314
using Microsoft.Extensions.Configuration;
1415

1516
namespace Microsoft.Extensions.Azure
@@ -106,7 +107,11 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
106107
var systemAccessToken = configuration["systemAccessToken"];
107108
var additionallyAllowedTenants = configuration["additionallyAllowedTenants"];
108109
var tokenFilePath = configuration["tokenFilePath"];
110+
var azureCloud = configuration["azureCloud"];
111+
var managedIdentityClientId = configuration["managedIdentityClientId"];
112+
109113
IEnumerable<string> additionallyAllowedTenantsList = null;
114+
110115
if (!string.IsNullOrWhiteSpace(additionallyAllowedTenants))
111116
{
112117
// not relying on StringSplitOptions.RemoveEmptyEntries as we want to remove leading/trailing whitespace between entries
@@ -117,15 +122,7 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
117122

118123
if (string.Equals(credentialType, "managedidentity", StringComparison.OrdinalIgnoreCase))
119124
{
120-
int idCount = 0;
121-
idCount += string.IsNullOrWhiteSpace(clientId) ? 0 : 1;
122-
idCount += string.IsNullOrWhiteSpace(resourceId) ? 0 : 1;
123-
idCount += string.IsNullOrWhiteSpace(objectId) ? 0 : 1;
124-
125-
if (idCount > 1)
126-
{
127-
throw new ArgumentException("Only one of either 'clientId', 'managedIdentityResourceId', or 'managedIdentityObjectId' can be specified for managed identity.");
128-
}
125+
AssertSingleManagedIdentityIdentifier(clientId, managedIdentityClientId, resourceId, objectId, isFederated: false);
129126

130127
if (!string.IsNullOrWhiteSpace(resourceId))
131128
{
@@ -137,6 +134,11 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
137134
return new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedObjectId(objectId));
138135
}
139136

137+
if (!string.IsNullOrWhiteSpace(managedIdentityClientId))
138+
{
139+
return new ManagedIdentityCredential(managedIdentityClientId);
140+
}
141+
140142
return new ManagedIdentityCredential(clientId);
141143
}
142144

@@ -145,6 +147,7 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
145147
// The WorkloadIdentityCredentialOptions object initialization populates its instance members
146148
// from the environment variables AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_FEDERATED_TOKEN_FILE
147149
var workloadIdentityOptions = new WorkloadIdentityCredentialOptions();
150+
148151
if (!string.IsNullOrWhiteSpace(tenantId))
149152
{
150153
workloadIdentityOptions.TenantId = tenantId;
@@ -178,6 +181,33 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
178181
throw new ArgumentException("For workload identity, 'tenantId', 'clientId', and 'tokenFilePath' must be specified via environment variables or the configuration.");
179182
}
180183

184+
if (string.Equals(credentialType, "managedidentityasfederatedidentity", StringComparison.OrdinalIgnoreCase))
185+
{
186+
AssertSingleManagedIdentityIdentifier(clientId, managedIdentityClientId, resourceId, objectId, isFederated: true);
187+
188+
if (string.IsNullOrWhiteSpace(tenantId) ||
189+
string.IsNullOrWhiteSpace(clientId) ||
190+
string.IsNullOrWhiteSpace(azureCloud))
191+
{
192+
throw new ArgumentException("For managed identity as a federated identity credential, 'tenantId', 'clientId', 'azureCloud', and one of ['managedIdentityClientId', 'resourceId', 'objectId'] must be specified via environment variables or the configuration.");
193+
}
194+
195+
if (!string.IsNullOrWhiteSpace(resourceId))
196+
{
197+
return new ManagedFederatedIdentityCredential(tenantId, clientId, new ResourceIdentifier(resourceId), azureCloud, additionallyAllowedTenantsList);
198+
}
199+
200+
if (!string.IsNullOrWhiteSpace(objectId))
201+
{
202+
return new ManagedFederatedIdentityCredential(tenantId, clientId, ManagedIdentityId.FromUserAssignedObjectId(objectId), azureCloud, additionallyAllowedTenantsList);
203+
}
204+
205+
if (!string.IsNullOrWhiteSpace(managedIdentityClientId))
206+
{
207+
return new ManagedFederatedIdentityCredential(tenantId, clientId, managedIdentityClientId, azureCloud, additionallyAllowedTenantsList);
208+
}
209+
}
210+
181211
if (string.Equals(credentialType, "azurepipelines", StringComparison.OrdinalIgnoreCase))
182212
{
183213
if (string.IsNullOrWhiteSpace(tenantId) ||
@@ -258,8 +288,6 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
258288
return credential;
259289
}
260290

261-
// TODO: More logging
262-
263291
if (!string.IsNullOrWhiteSpace(objectId))
264292
{
265293
throw new ArgumentException("'managedIdentityObjectId' is only supported when the credential type is 'managedidentity'.");
@@ -268,6 +296,7 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
268296
if (additionallyAllowedTenantsList != null
269297
|| !string.IsNullOrWhiteSpace(tenantId)
270298
|| !string.IsNullOrWhiteSpace(clientId)
299+
|| !string.IsNullOrWhiteSpace(managedIdentityClientId)
271300
|| !string.IsNullOrWhiteSpace(resourceId))
272301
{
273302
var options = new DefaultAzureCredentialOptions();
@@ -285,7 +314,11 @@ internal static TokenCredential CreateCredential(IConfiguration configuration)
285314
options.TenantId = tenantId;
286315
}
287316

288-
if (!string.IsNullOrWhiteSpace(clientId))
317+
if (!string.IsNullOrWhiteSpace(managedIdentityClientId))
318+
{
319+
options.ManagedIdentityClientId = managedIdentityClientId;
320+
}
321+
else if (!string.IsNullOrWhiteSpace(clientId))
289322
{
290323
options.ManagedIdentityClientId = clientId;
291324
}
@@ -366,6 +399,34 @@ internal static object CreateClientOptions(
366399
return Activator.CreateInstance(optionsType, constructorArguments);
367400
}
368401

402+
private static void AssertSingleManagedIdentityIdentifier(string clientId, string managedIdentityClientId, string resourceId, string objectId, bool isFederated = false)
403+
{
404+
var idCount = 0;
405+
406+
if (!isFederated)
407+
{
408+
idCount += string.IsNullOrWhiteSpace(clientId) ? 0 : 1;
409+
}
410+
411+
idCount += string.IsNullOrWhiteSpace(managedIdentityClientId) ? 0 : 1;
412+
idCount += string.IsNullOrWhiteSpace(resourceId) ? 0 : 1;
413+
idCount += string.IsNullOrWhiteSpace(objectId) ? 0 : 1;
414+
415+
var validIdentifiers = isFederated
416+
? "'clientId', 'managedIdentityClientId', 'managedIdentityResourceId', or 'managedIdentityObjectId'"
417+
: "'managedIdentityClientId', 'managedIdentityResourceId', or 'managedIdentityObjectId'";
418+
419+
if (idCount > 1)
420+
{
421+
throw new ArgumentException($"Only one of [{validIdentifiers}] can be specified for managed identity.");
422+
}
423+
424+
if (isFederated && idCount < 1)
425+
{
426+
throw new ArgumentException($"At least one of [{validIdentifiers}] must be specified for managed identity.");
427+
}
428+
}
429+
369430
private static bool IsServiceVersionParameter(ParameterInfo parameter) =>
370431
parameter.ParameterType.Name == ServiceVersionParameterTypeName;
371432

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Azure.Core;
9+
using Azure.Identity;
10+
11+
namespace Microsoft.Extensions.Azure.Internal
12+
{
13+
internal class ManagedFederatedIdentityCredential : TokenCredential
14+
{
15+
private readonly ManagedIdentityCredential _managedIdentityCredential;
16+
private readonly ClientAssertionCredential _clientAssertionCredential;
17+
private readonly TokenRequestContext _tokenContext;
18+
19+
/// <summary>
20+
/// Gets the set of additionally allowed tenants.
21+
/// </summary>
22+
public IList<string> AdditionallyAllowedTenants { get; }
23+
24+
/// <summary>
25+
/// Creates an instance of the ManagedFederatedIdentityCredential with a synchronous callback that provides a signed client assertion to authenticate against Microsoft Entra ID.
26+
/// </summary>
27+
/// <param name="tenantId">The Microsoft Entra tenant (directory) ID of the service principal.</param>
28+
/// <param name="clientId">The client (application) ID of the service principal.</param>
29+
/// <param name="managedIdentityId">The user-assigned managed identity which has been configured as a Federated Identity Credential (FIC). May be a client id, resource id, or object id.</param>
30+
/// <param name="azureCloud">
31+
/// The name of the cloud where the managed identity is configured. Valid values are:
32+
/// <list type="bullet">
33+
/// <item>
34+
/// <term>public</term>
35+
/// <description>Entra ID Global cloud</description>
36+
/// </item>
37+
/// <item>
38+
/// <term>usgov</term>
39+
/// <description>Entra ID US Government</description>
40+
/// </item>
41+
/// <item>
42+
/// <term>china</term>
43+
/// <description>Entra ID China operated by 21Vianet</description>
44+
/// </item>
45+
/// </list>
46+
/// </param>
47+
/// <param name="additionallyAllowedTenants">The set of </param>
48+
public ManagedFederatedIdentityCredential(string tenantId, string clientId, string managedIdentityId, string azureCloud, IEnumerable<string> additionallyAllowedTenants = default)
49+
: this(tenantId, clientId, (object)managedIdentityId, azureCloud, additionallyAllowedTenants)
50+
{
51+
}
52+
53+
/// <summary>
54+
/// Creates an instance of the ManagedFederatedIdentityCredential with a synchronous callback that provides a signed client assertion to authenticate against Microsoft Entra ID.
55+
/// </summary>
56+
/// <param name="tenantId">The Microsoft Entra tenant (directory) ID of the service principal.</param>
57+
/// <param name="clientId">The client (application) ID of the service principal.</param>
58+
/// <param name="managedIdentityId">The user-assigned managed identity which has been configured as a Federated Identity Credential (FIC). May be a client id, resource id, or object id.</param>
59+
/// <param name="azureCloud">
60+
/// The name of the cloud where the managed identity is configured. Valid values are:
61+
/// <list type="bullet">
62+
/// <item>
63+
/// <term>public</term>
64+
/// <description>Entra ID Global cloud</description>
65+
/// </item>
66+
/// <item>
67+
/// <term>usgov</term>
68+
/// <description>Entra ID US Government</description>
69+
/// </item>
70+
/// <item>
71+
/// <term>china</term>
72+
/// <description>Entra ID China operated by 21Vianet</description>
73+
/// </item>
74+
/// </list>
75+
/// </param>
76+
/// <param name="additionallyAllowedTenants">The set of </param>
77+
public ManagedFederatedIdentityCredential(string tenantId, string clientId, ResourceIdentifier managedIdentityId, string azureCloud, IEnumerable<string> additionallyAllowedTenants = default)
78+
: this(tenantId, clientId, (object)managedIdentityId, azureCloud, additionallyAllowedTenants)
79+
{
80+
}
81+
82+
/// <summary>
83+
/// Creates an instance of the ManagedFederatedIdentityCredential with a synchronous callback that provides a signed client assertion to authenticate against Microsoft Entra ID.
84+
/// </summary>
85+
/// <param name="tenantId">The Microsoft Entra tenant (directory) ID of the service principal.</param>
86+
/// <param name="clientId">The client (application) ID of the service principal.</param>
87+
/// <param name="managedIdentityId">The user-assigned managed identity which has been configured as a Federated Identity Credential (FIC). May be a client id, resource id, or object id.</param>
88+
/// <param name="azureCloud">
89+
/// The name of the cloud where the managed identity is configured. Valid values are:
90+
/// <list type="bullet">
91+
/// <item>
92+
/// <term>public</term>
93+
/// <description>Entra ID Global cloud</description>
94+
/// </item>
95+
/// <item>
96+
/// <term>usgov</term>
97+
/// <description>Entra ID US Government</description>
98+
/// </item>
99+
/// <item>
100+
/// <term>china</term>
101+
/// <description>Entra ID China operated by 21Vianet</description>
102+
/// </item>
103+
/// </list>
104+
/// </param>
105+
/// <param name="additionallyAllowedTenants">The set of </param>
106+
public ManagedFederatedIdentityCredential(string tenantId, string clientId, ManagedIdentityId managedIdentityId, string azureCloud, IEnumerable<string> additionallyAllowedTenants = default)
107+
: this(tenantId, clientId, (object)managedIdentityId, azureCloud, additionallyAllowedTenants)
108+
{
109+
}
110+
111+
internal ManagedFederatedIdentityCredential(string tenantId, string clientId, object managedIdentityId, string azureCloud, IEnumerable<string> additionallyAllowedTenants = default)
112+
{
113+
ClientAssertionCredentialOptions clientAssertionOptions = null;
114+
115+
if (additionallyAllowedTenants != null)
116+
{
117+
clientAssertionOptions = new();
118+
119+
foreach (var tenant in additionallyAllowedTenants)
120+
{
121+
clientAssertionOptions.AdditionallyAllowedTenants.Add(tenant);
122+
}
123+
124+
AdditionallyAllowedTenants = clientAssertionOptions.AdditionallyAllowedTenants;
125+
}
126+
else
127+
{
128+
AdditionallyAllowedTenants = new List<string>();
129+
}
130+
131+
_managedIdentityCredential = managedIdentityId switch
132+
{
133+
ManagedIdentityId objectId => new ManagedIdentityCredential(objectId),
134+
ResourceIdentifier resourceId => new ManagedIdentityCredential(resourceId),
135+
string managedClientId => new ManagedIdentityCredential(managedClientId),
136+
_ => throw new ArgumentException($"Invalid managed identity ID type: {managedIdentityId.GetType()}", nameof(managedIdentityId))
137+
};
138+
139+
_tokenContext = new TokenRequestContext([TranslateCloudToTokenScope(azureCloud)]);
140+
_clientAssertionCredential = new ClientAssertionCredential(
141+
tenantId,
142+
clientId,
143+
async _ =>
144+
(await _managedIdentityCredential
145+
.GetTokenAsync(_tokenContext)
146+
.ConfigureAwait(false))
147+
.Token,
148+
clientAssertionOptions
149+
);
150+
}
151+
152+
/// <summary>
153+
/// Initializes a new instance of the <see cref="ManagedFederatedIdentityCredential"/> class.
154+
/// </summary>
155+
protected ManagedFederatedIdentityCredential()
156+
{
157+
}
158+
159+
/// <inheritdoc />
160+
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => _clientAssertionCredential.GetToken(requestContext, cancellationToken);
161+
162+
/// <inheritdoc />
163+
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) => _clientAssertionCredential.GetTokenAsync(requestContext, cancellationToken);
164+
165+
private static string TranslateCloudToTokenScope(string azureCloud) =>
166+
azureCloud switch
167+
{
168+
AzureCloud.Public => "api://AzureADTokenExchange/.default",
169+
AzureCloud.USGov => "api://AzureADTokenExchangeUSGov/.default",
170+
AzureCloud.China => "api://AzureADTokenExchangeChina/.default",
171+
_ => throw new ArgumentException($"Unknown Azure cloud: {azureCloud}", nameof(azureCloud)),
172+
};
173+
}
174+
}

0 commit comments

Comments
 (0)