|
3 | 3 | using System.IO;
|
4 | 4 | using System.Linq;
|
5 | 5 | using System.Net.Http;
|
| 6 | +using System.Security.Cryptography.X509Certificates; |
6 | 7 | using System.Threading.Tasks;
|
7 | 8 | using GitCredentialManager.Interop.Windows.Native;
|
8 | 9 | using Microsoft.Identity.Client;
|
@@ -35,6 +36,43 @@ public interface IMicrosoftAuthentication
|
35 | 36 | /// <returns>Authentication result.</returns>
|
36 | 37 | Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, string clientId, Uri redirectUri,
|
37 | 38 | 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; } |
38 | 76 | }
|
39 | 77 |
|
40 | 78 | public interface IMicrosoftAuthenticationResult
|
@@ -210,6 +248,23 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(
|
210 | 248 | }
|
211 | 249 | }
|
212 | 250 |
|
| 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 | + |
213 | 268 | private async Task<bool> UseDefaultAccountAsync(string userName)
|
214 | 269 | {
|
215 | 270 | ThrowIfUserInteractionDisabled();
|
@@ -428,6 +483,35 @@ private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
|
428 | 483 | return app;
|
429 | 484 | }
|
430 | 485 |
|
| 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 | + |
431 | 515 | #endregion
|
432 | 516 |
|
433 | 517 | #region Helpers
|
|
0 commit comments