Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
msauth: add support for managed identity
Add support for obtaining an access token using either the
system-assigned and a user-assigned managed identity.
  • Loading branch information
mjcheetham committed Aug 15, 2023
commit bfa87dba093bbb8a2ffc28288d0b3c3b10fae1c9
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using System;
using System.Threading.Tasks;
using GitCredentialManager.Authentication;
using GitCredentialManager.Tests.Objects;
using Microsoft.Identity.Client.AppConfig;
using Xunit;

namespace GitCredentialManager.Tests.Authentication
{
public class MicrosoftAuthenticationTests
{
[Fact]
public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException()
public async Task MicrosoftAuthentication_GetTokenForUserAsync_NoInteraction_ThrowsException()
{
const string authority = "https://login.microsoftonline.com/common";
const string clientId = "C9E8FDA6-1D46-484C-917C-3DBD518F27C3";
Expand All @@ -26,5 +28,46 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetTokenForUser
await Assert.ThrowsAsync<Trace2InvalidOperationException>(
() => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("system")]
[InlineData("SYSTEM")]
[InlineData("sYsTeM")]
[InlineData("00000000-0000-0000-0000-000000000000")]
[InlineData("id://00000000-0000-0000-0000-000000000000")]
[InlineData("ID://00000000-0000-0000-0000-000000000000")]
[InlineData("Id://00000000-0000-0000-0000-000000000000")]
public void MicrosoftAuthentication_GetManagedIdentity_ValidSystemId_ReturnsSystemId(string str)
{
ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);
Assert.Equal(ManagedIdentityId.SystemAssigned, actual);
}

[Theory]
[InlineData("8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("id://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("ID://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("Id://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("resource://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("RESOURCE://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("rEsOuRcE://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("resource://00000000-0000-0000-0000-000000000000")]
public void MicrosoftAuthentication_GetManagedIdentity_ValidUserIdByClientId_ReturnsUserId(string str)
{
ManagedIdentityId actual = MicrosoftAuthentication.GetManagedIdentity(str);
Assert.NotNull(actual);
Assert.NotEqual(ManagedIdentityId.SystemAssigned, actual);
}

[Theory]
[InlineData("unknown://8B49DCA0-1298-4A0D-AD6D-934E40230839")]
[InlineData("this is a string")]
public void MicrosoftAuthentication_GetManagedIdentity_Invalid_ThrowsArgumentException(string str)
{
Assert.Throws<ArgumentException>(() => MicrosoftAuthentication.GetManagedIdentity(str));
}
}
}
91 changes: 90 additions & 1 deletion src/shared/Core/Authentication/MicrosoftAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using GitCredentialManager.UI;
using GitCredentialManager.UI.ViewModels;
using GitCredentialManager.UI.Views;
using Microsoft.Identity.Client.AppConfig;

#if NETFRAMEWORK
using System.Drawing;
Expand Down Expand Up @@ -44,6 +45,25 @@ Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, stri
/// <param name="scopes">Scopes to request.</param>
/// <returns>Authentication result.</returns>
Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes);

/// <summary>
/// Acquire a token using the managed identity in the current environment.
/// </summary>
/// <param name="managedIdentity">Managed identity to use.</param>
/// <param name="resource">Resource to obtain an access token for.</param>
/// <returns>Authentication result including access token.</returns>
/// <remarks>
/// There are several formats for the <paramref name="managedIdentity"/> parameter:
/// <para/>
/// - <c>"system"</c> - Use the system-assigned managed identity.
/// <para/>
/// - <c>"{guid}"</c> - Use the user-assigned managed identity with client ID <c>{guid}</c>.
/// <para/>
/// - <c>"id://{guid}"</c> - Use the user-assigned managed identity with client ID <c>{guid}</c>.
/// <para/>
/// - <c>"resource://{guid}"</c> - Use the user-assigned managed identity with resource ID <c>{guid}</c>.
/// </remarks>
Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsync(string managedIdentity, string resource);
}

public class ServicePrincipalIdentity
Expand Down Expand Up @@ -265,6 +285,31 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsy
}
}

public async Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsync(string managedIdentity, string resource)
{
var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);

ManagedIdentityId mid = GetManagedIdentity(managedIdentity);

IManagedIdentityApplication app = ManagedIdentityApplicationBuilder.Create(mid)
.WithHttpClientFactory(httpFactoryAdaptor)
.Build();

try
{
AuthenticationResult result = await app.AcquireTokenForManagedIdentity(resource).ExecuteAsync();
return new MsalResult(result);
}
catch (Exception ex)
{
Context.Trace.WriteLine(mid == ManagedIdentityId.SystemAssigned
? "Failed to acquire token for system managed identity."
: $"Failed to acquire token for user managed identity '{managedIdentity:D}'.");
Context.Trace.WriteException(ex);
throw;
}
}

private async Task<bool> UseDefaultAccountAsync(string userName)
{
ThrowIfUserInteractionDisabled();
Expand Down Expand Up @@ -624,6 +669,50 @@ internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallba
return builder.Build();
}

internal static ManagedIdentityId GetManagedIdentity(string str)
{
// An empty string or "system" means system-assigned managed identity
if (string.IsNullOrWhiteSpace(str) || str.Equals("system", StringComparison.OrdinalIgnoreCase))
{
return ManagedIdentityId.SystemAssigned;
}

//
// A GUID-looking value means a user-assigned managed identity specified by the client ID.
// If the "{value}" is the empty GUID then we use the system-assigned MI.
//
if (Guid.TryParse(str, out Guid guid))
{
return guid == Guid.Empty
? ManagedIdentityId.SystemAssigned
: ManagedIdentityId.WithUserAssignedClientId(str);
}

//
// A value of the form "id://{value}" means a user-assigned managed identity specified by the client ID.
// If the "{value}" is the empty GUID then we use the system-assigned MI.
//
// If the value is "resource://{value}" then it is a user-assigned managed identity specified
// by the resource ID.
//
if (Uri.TryCreate(str, UriKind.Absolute, out Uri uri))
{
if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "id"))
{
return Guid.TryParse(uri.Host, out Guid g) && g == Guid.Empty
? ManagedIdentityId.SystemAssigned
: ManagedIdentityId.WithUserAssignedClientId(uri.Host);
}

if (StringComparer.OrdinalIgnoreCase.Equals(uri.Scheme, "resource"))
{
return ManagedIdentityId.WithUserAssignedResourceId(uri.Host);
}
}

throw new ArgumentException("Invalid managed identity value.", nameof(str));
}

private static EmbeddedWebViewOptions GetEmbeddedWebViewOptions()
{
return new EmbeddedWebViewOptions
Expand Down Expand Up @@ -774,7 +863,7 @@ public MsalResult(AuthenticationResult msalResult)
}

public string AccessToken => _msalResult.AccessToken;
public string AccountUpn => _msalResult.Account.Username;
public string AccountUpn => _msalResult.Account?.Username;
}

#if NETFRAMEWORK
Expand Down