Skip to content

Commit bfa87db

Browse files
committed
msauth: add support for managed identity
Add support for obtaining an access token using either the system-assigned and a user-assigned managed identity.
1 parent 6a90c36 commit bfa87db

File tree

2 files changed

+134
-2
lines changed

2 files changed

+134
-2
lines changed

src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
using System;
2+
using System.Threading.Tasks;
23
using GitCredentialManager.Authentication;
34
using GitCredentialManager.Tests.Objects;
5+
using Microsoft.Identity.Client.AppConfig;
46
using Xunit;
57

68
namespace GitCredentialManager.Tests.Authentication
79
{
810
public class MicrosoftAuthenticationTests
911
{
1012
[Fact]
11-
public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException()
13+
public async Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException()
1214
{
1315
const string authority = "https://login.microsoftonline.com/common";
1416
const string clientId = "C9E8FDA6-1D46-484C-917C-3DBD518F27C3";
@@ -26,5 +28,46 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUser
2628
await Assert.ThrowsAsync<Trace2InvalidOperationException>(
2729
() => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false));
2830
}
31+
32+
[Theory]
33+
[InlineData(null)]
34+
[InlineData("")]
35+
[InlineData(" ")]
36+
[InlineData("system")]
37+
[InlineData("SYSTEM")]
38+
[InlineData("sYsTeM")]
39+
[InlineData("00000000-0000-0000-0000-000000000000")]
40+
[InlineData("id://00000000-0000-0000-0000-000000000000")]
41+
[InlineData("ID://00000000-0000-0000-0000-000000000000")]
42+
[InlineData("Id://00000000-0000-0000-0000-000000000000")]
43+
public void MicrosoftAuthentication_GetManagedIdentity_ValidSystemId_ReturnsSystemId(string str)
44+
{
45+
ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);
46+
Assert.Equal(ManagedIdentityId.SystemAssigned, actual);
47+
}
48+
49+
[Theory]
50+
[InlineData("8B49DCA0-1298-4A0D-AD6D-934E40230839")]
51+
[InlineData("id://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
52+
[InlineData("ID://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
53+
[InlineData("Id://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
54+
[InlineData("resource://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
55+
[InlineData("RESOURCE://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
56+
[InlineData("rEsOuRcE://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
57+
[InlineData("resource://00000000-0000-0000-0000-000000000000")]
58+
public void MicrosoftAuthentication_GetManagedIdentity_ValidUserIdByClientId_ReturnsUserId(string str)
59+
{
60+
ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);
61+
Assert.NotNull(actual);
62+
Assert.NotEqual(ManagedIdentityId.SystemAssigned, actual);
63+
}
64+
65+
[Theory]
66+
[InlineData("unknown://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
67+
[InlineData("this is a string")]
68+
public void MicrosoftAuthentication_GetManagedIdentity_Invalid_ThrowsArgumentException(string str)
69+
{
70+
Assert.Throws<ArgumentException>(() => MicrosoftAuthentication.GetManagedIdentity(str));
71+
}
2972
}
3073
}

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using GitCredentialManager.UI;
1414
using GitCredentialManager.UI.ViewModels;
1515
using GitCredentialManager.UI.Views;
16+
using Microsoft.Identity.Client.AppConfig;
1617

1718
#if NETFRAMEWORK
1819
using System.Drawing;
@@ -44,6 +45,25 @@ Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, stri
4445
/// <param name="scopes">Scopes to request.</param>
4546
/// <returns>Authentication result.</returns>
4647
Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes);
48+
49+
/// <summary>
50+
/// Acquire a token using the managed identity in the current environment.
51+
/// </summary>
52+
/// <param name="managedIdentity">Managed identity to use.</param>
53+
/// <param name="resource">Resource to obtain an access token for.</param>
54+
/// <returns>Authentication result including access token.</returns>
55+
/// <remarks>
56+
/// There are several formats for the <paramref name="managedIdentity"/> parameter:
57+
/// <para/>
58+
/// - <c>"system"</c> - Use the system-assigned managed identity.
59+
/// <para/>
60+
/// - <c>"{guid}"</c> - Use the user-assigned managed identity with client ID <c>{guid}</c>.
61+
/// <para/>
62+
/// - <c>"id://{guid}"</c> - Use the user-assigned managed identity with client ID <c>{guid}</c>.
63+
/// <para/>
64+
/// - <c>"resource://{guid}"</c> - Use the user-assigned managed identity with resource ID <c>{guid}</c>.
65+
/// </remarks>
66+
Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsync(string managedIdentity, string resource);
4767
}
4868

4969
public class ServicePrincipalIdentity
@@ -265,6 +285,31 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsy
265285
}
266286
}
267287

288+
public async Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsync(string managedIdentity, string resource)
289+
{
290+
var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);
291+
292+
ManagedIdentityId mid = GetManagedIdentity(managedIdentity);
293+
294+
IManagedIdentityApplication app = ManagedIdentityApplicationBuilder.Create(mid)
295+
.WithHttpClientFactory(httpFactoryAdaptor)
296+
.Build();
297+
298+
try
299+
{
300+
AuthenticationResult result = await app.AcquireTokenForManagedIdentity(resource).ExecuteAsync();
301+
return new MsalResult(result);
302+
}
303+
catch (Exception ex)
304+
{
305+
Context.Trace.WriteLine(mid == ManagedIdentityId.SystemAssigned
306+
? "Failed to acquire token for system managed identity."
307+
: $"Failed to acquire token for user managed identity '{managedIdentity:D}'.");
308+
Context.Trace.WriteException(ex);
309+
throw;
310+
}
311+
}
312+
268313
private async Task<bool> UseDefaultAccountAsync(string userName)
269314
{
270315
ThrowIfUserInteractionDisabled();
@@ -624,6 +669,50 @@ internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallba
624669
return builder.Build();
625670
}
626671

672+
internal static ManagedIdentityId GetManagedIdentity(string str)
673+
{
674+
// An empty string or "system" means system-assigned managed identity
675+
if (string.IsNullOrWhiteSpace(str) || str.Equals("system", StringComparison.OrdinalIgnoreCase))
676+
{
677+
return ManagedIdentityId.SystemAssigned;
678+
}
679+
680+
//
681+
// A GUID-looking value means a user-assigned managed identity specified by the client ID.
682+
// If the "{value}" is the empty GUID then we use the system-assigned MI.
683+
//
684+
if (Guid.TryParse(str, out Guid guid))
685+
{
686+
return guid == Guid.Empty
687+
? ManagedIdentityId.SystemAssigned
688+
: ManagedIdentityId.WithUserAssignedClientId(str);
689+
}
690+
691+
//
692+
// A value of the form "id://{value}" means a user-assigned managed identity specified by the client ID.
693+
// If the "{value}" is the empty GUID then we use the system-assigned MI.
694+
//
695+
// If the value is "resource://{value}" then it is a user-assigned managed identity specified
696+
// by the resource ID.
697+
//
698+
if (Uri.TryCreate(str, UriKind.Absolute, out Uri uri))
699+
{
700+
if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "id"))
701+
{
702+
return Guid.TryParse(uri.Host, out Guid g) && g == Guid.Empty
703+
? ManagedIdentityId.SystemAssigned
704+
: ManagedIdentityId.WithUserAssignedClientId(uri.Host);
705+
}
706+
707+
if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "resource"))
708+
{
709+
return ManagedIdentityId.WithUserAssignedResourceId(uri.Host);
710+
}
711+
}
712+
713+
throw new ArgumentException("Invalid managed identity value.", nameof(str));
714+
}
715+
627716
private static EmbeddedWebViewOptions GetEmbeddedWebViewOptions()
628717
{
629718
return new EmbeddedWebViewOptions
@@ -774,7 +863,7 @@ public MsalResult(AuthenticationResult msalResult)
774863
}
775864

776865
public string AccessToken => _msalResult.AccessToken;
777-
public string AccountUpn => _msalResult.Account.Username;
866+
public string AccountUpn => _msalResult.Account?.Username;
778867
}
779868

780869
#if NETFRAMEWORK

0 commit comments

Comments
 (0)