Skip to content

Commit 6a90c36

Browse files
committed
msauth: add support for service principal auth
Add support for acquiring a token for a service principal. Either a client secret or certificate can be used to authenticate (the latter being preferred).
1 parent 89b099e commit 6a90c36

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using System.Net.Http;
6+
using System.Security.Cryptography.X509Certificates;
67
using System.Threading.Tasks;
78
using GitCredentialManager.Interop.Windows.Native;
89
using Microsoft.Identity.Client;
@@ -35,6 +36,43 @@ public interface IMicrosoftAuthentication
3536
/// <returns>Authentication result.</returns>
3637
Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, string clientId, Uri redirectUri,
3738
string[] scopes, string userName, bool msaPt = false);
39+
40+
/// <summary>
41+
/// Acquire an access token for the given service principal with the specified scopes.
42+
/// </summary>
43+
/// <param name="sp">Service principal identity.</param>
44+
/// <param name="scopes">Scopes to request.</param>
45+
/// <returns>Authentication result.</returns>
46+
Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes);
47+
}
48+
49+
public class ServicePrincipalIdentity
50+
{
51+
/// <summary>
52+
/// Client ID of the service principal.
53+
/// </summary>
54+
public string Id { get; set; }
55+
56+
/// <summary>
57+
/// Tenant ID of the service principal.
58+
/// </summary>
59+
public string TenantId { get; set; }
60+
61+
/// <summary>
62+
/// Certificate used to authenticate the service principal.
63+
/// </summary>
64+
/// <remarks>
65+
/// If both <see cref="Certificate"/> and <see cref="ClientSecret"/> are set, the certificate will be used.
66+
/// </remarks>
67+
public X509Certificate2 Certificate { get; set; }
68+
69+
/// <summary>
70+
/// Secret used to authenticate the service principal.
71+
/// </summary>
72+
/// <remarks>
73+
/// If both <see cref="Certificate"/> and <see cref="ClientSecret"/> are set, the certificate will be used.
74+
/// </remarks>
75+
public string ClientSecret { get; set; }
3876
}
3977

4078
public interface IMicrosoftAuthenticationResult
@@ -210,6 +248,23 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(
210248
}
211249
}
212250

251+
public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsync(ServicePrincipalIdentity sp, string[] scopes)
252+
{
253+
IConfidentialClientApplication app = CreateConfidentialClientApplication(sp);
254+
255+
try
256+
{
257+
AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
258+
return new MsalResult(result);
259+
}
260+
catch (Exception ex)
261+
{
262+
Context.Trace.WriteLine($"Failed to acquire token for service principal '{sp.TenantId}/{sp.TenantId}'.");
263+
Context.Trace.WriteException(ex);
264+
throw;
265+
}
266+
}
267+
213268
private async Task<bool> UseDefaultAccountAsync(string userName)
214269
{
215270
ThrowIfUserInteractionDisabled();
@@ -428,6 +483,35 @@ private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
428483
return app;
429484
}
430485

486+
private IConfidentialClientApplication CreateConfidentialClientApplication(ServicePrincipalIdentity sp)
487+
{
488+
var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);
489+
490+
Context.Trace.WriteLine($"Creating confidential client application for {sp.TenantId}/{sp.Id}...");
491+
var appBuilder = ConfidentialClientApplicationBuilder.Create(sp.Id)
492+
.WithTenantId(sp.TenantId)
493+
.WithHttpClientFactory(httpFactoryAdaptor);
494+
495+
if (sp.Certificate is not null)
496+
{
497+
Context.Trace.WriteLineSecrets("Using certificate with thumbprint: '{0}'", new object[] { sp.Certificate.Thumbprint });
498+
appBuilder = appBuilder.WithCertificate(sp.Certificate);
499+
}
500+
else if (!string.IsNullOrWhiteSpace(sp.ClientSecret))
501+
{
502+
Context.Trace.WriteLineSecrets("Using client secret: '{0}'", new object[] { sp.ClientSecret });
503+
appBuilder = appBuilder.WithClientSecret(sp.ClientSecret);
504+
}
505+
else
506+
{
507+
throw new InvalidOperationException("Service principal identity does not contain a certificate or client secret.");
508+
}
509+
510+
IConfidentialClientApplication app = appBuilder.Build();
511+
512+
return app;
513+
}
514+
431515
#endregion
432516

433517
#region Helpers

0 commit comments

Comments
 (0)