99using Microsoft . Identity . Client ;
1010using Microsoft . Identity . Client . Extensions . Msal ;
1111using System . Text ;
12+ using System . Text . Json ;
1213using System . Threading ;
1314using GitCredentialManager . UI ;
1415using GitCredentialManager . UI . Controls ;
@@ -63,6 +64,14 @@ Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, stri
6364 /// - <c>"resource://{guid}"</c> - Use the user-assigned managed identity with resource ID <c>{guid}</c>.
6465 /// </remarks>
6566 Task < IMicrosoftAuthenticationResult > GetTokenForManagedIdentityAsync ( string managedIdentity , string resource ) ;
67+
68+ /// <summary>
69+ /// Acquire a token using workload federation.
70+ /// </summary>
71+ /// <param name="fedOpts">An object containing configuration workload federation.</param>
72+ /// <param name="scopes">Scopes to request.</param>
73+ /// <returns>Authentication result including access token.</returns>
74+ Task < IMicrosoftAuthenticationResult > GetTokenUsingWorkloadFederationAsync ( MicrosoftWorkloadFederationOptions fedOpts , string [ ] scopes ) ;
6675 }
6776
6877 public class ServicePrincipalIdentity
@@ -287,7 +296,8 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsy
287296 }
288297 }
289298
290- public async Task < IMicrosoftAuthenticationResult > GetTokenForManagedIdentityAsync ( string managedIdentity , string resource )
299+ public async Task < IMicrosoftAuthenticationResult > GetTokenForManagedIdentityAsync (
300+ string managedIdentity , string resource )
291301 {
292302 var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor ( Context . HttpClientFactory ) ;
293303
@@ -306,8 +316,88 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsyn
306316 {
307317 Context . Trace . WriteLine ( mid == ManagedIdentityId . SystemAssigned
308318 ? "Failed to acquire token for system managed identity."
309- : $ "Failed to acquire token for user managed identity '{ managedIdentity : D} '.") ;
319+ : $ "Failed to acquire token for user managed identity '{ managedIdentity } '.") ;
320+ Context . Trace . WriteException ( ex ) ;
321+ throw ;
322+ }
323+ }
324+
325+ public async Task < IMicrosoftAuthenticationResult > GetTokenUsingWorkloadFederationAsync ( MicrosoftWorkloadFederationOptions fedOpts , string [ ] scopes )
326+ {
327+ IConfidentialClientApplication app = await CreateConfidentialClientApplicationAsync ( fedOpts ) ;
328+
329+ AuthenticationResult result = await app . AcquireTokenForClient ( scopes )
330+ . ExecuteAsync ( )
331+ . ConfigureAwait ( false ) ;
332+
333+ return new MsalResult ( result ) ;
334+ }
335+
336+ private async Task < string > GetClientAssertion ( MicrosoftWorkloadFederationOptions fedOpts , AssertionRequestOptions _ )
337+ {
338+ switch ( fedOpts . Scenario )
339+ {
340+ case MicrosoftWorkloadFederationScenario . Generic :
341+ Context . Trace . WriteLine ( "Getting client assertion for generic workload federation scenario..." ) ;
342+ if ( string . IsNullOrWhiteSpace ( fedOpts . GenericClientAssertion ) )
343+ throw new InvalidOperationException (
344+ "Client assertion must be provided for generic workload federation scenario." ) ;
345+ return fedOpts . GenericClientAssertion ;
346+
347+ case MicrosoftWorkloadFederationScenario . ManagedIdentity :
348+ Context . Trace . WriteLine ( "Getting client assertion for managed identity workload federation scenario..." ) ;
349+ var miResult = await GetTokenForManagedIdentityAsync ( fedOpts . ManagedIdentityId , fedOpts . Audience ) ;
350+ return miResult . AccessToken ;
351+
352+ case MicrosoftWorkloadFederationScenario . GitHubActions :
353+ Context . Trace . WriteLine ( "Getting client assertion for GitHub Actions workload federation scenario..." ) ;
354+ return await GetGitHubOidcToken ( fedOpts . GitHubTokenRequestUrl , fedOpts . Audience , fedOpts . GitHubTokenRequestToken ) ;
355+
356+ default :
357+ throw new ArgumentOutOfRangeException ( nameof ( fedOpts . Scenario ) , fedOpts . Scenario , "Unsupported workload federation scenario." ) ;
358+ }
359+ }
360+
361+ private async Task < string > GetGitHubOidcToken ( Uri requestUri , string audience , string requestToken )
362+ {
363+ using HttpClient http = Context . HttpClientFactory . CreateClient ( ) ;
364+
365+ UriBuilder ub = new UriBuilder ( requestUri ) ;
366+ if ( ub . Query . Length > 0 ) ub . Query += "&" ;
367+ ub . Query += $ "audience={ Uri . EscapeDataString ( audience ) } ";
368+
369+ using var request = new HttpRequestMessage ( HttpMethod . Get , ub . Uri ) ;
370+ request . AddBearerAuthenticationHeader ( requestToken ) ;
371+
372+ Context . Trace . WriteLine ( $ "Requesting GitHub OIDC token from '{ request . RequestUri } '...") ;
373+ Context . Trace . WriteLineSecrets ( "OIDC request token: {0}" , new [ ] { requestToken } ) ;
374+ using HttpResponseMessage response = await http . SendAsync ( request ) ;
375+ if ( ! response . IsSuccessStatusCode )
376+ {
377+ string error = await response . Content . ReadAsStringAsync ( ) ;
378+ Context . Trace . WriteLine ( $ "Failed to acquire GitHub OIDC token [{ response . StatusCode : D} { response . StatusCode } ]: { error } ") ;
379+ response . EnsureSuccessStatusCode ( ) ;
380+ }
381+
382+ string json = await response . Content . ReadAsStringAsync ( ) ;
383+
384+ try
385+ {
386+ using JsonDocument jsonDoc = JsonDocument . Parse ( json ) ;
387+ if ( ! jsonDoc . RootElement . TryGetProperty ( "value" , out JsonElement tokenElement ) )
388+ {
389+ throw new InvalidOperationException (
390+ "Invalid response from GitHub OIDC token endpoint: 'value' property not found." ) ;
391+ }
392+
393+ return tokenElement . GetString ( ) ??
394+ throw new InvalidOperationException (
395+ "Invalid response from GitHub OIDC token endpoint: 'value' property is null." ) ;
396+ }
397+ catch ( Exception ex )
398+ {
310399 Context . Trace . WriteException ( ex ) ;
400+ Context . Trace . WriteLine ( $ "OIDC token response: { json } ") ;
311401 throw ;
312402 }
313403 }
@@ -558,6 +648,24 @@ private async Task<IConfidentialClientApplication> CreateConfidentialClientAppli
558648 return app ;
559649 }
560650
651+ private async Task < IConfidentialClientApplication > CreateConfidentialClientApplicationAsync (
652+ MicrosoftWorkloadFederationOptions fedOpts )
653+ {
654+ var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor ( Context . HttpClientFactory ) ;
655+
656+ Context . Trace . WriteLine ( $ "Creating federated confidential client application for { fedOpts . TenantId } /{ fedOpts . ClientId } ...") ;
657+ var appBuilder = ConfidentialClientApplicationBuilder . Create ( fedOpts . ClientId )
658+ . WithTenantId ( fedOpts . TenantId )
659+ . WithHttpClientFactory ( httpFactoryAdaptor )
660+ . WithClientAssertion ( reqOpts => GetClientAssertion ( fedOpts , reqOpts ) ) ;
661+
662+ IConfidentialClientApplication app = appBuilder . Build ( ) ;
663+
664+ await RegisterTokenCacheAsync ( app . AppTokenCache , CreateAppTokenCacheProps , Context . Trace2 ) ;
665+
666+ return app ;
667+ }
668+
561669 #endregion
562670
563671 #region Helpers
0 commit comments