33using System . IO ;
44using System . Linq ;
55using System . Net . Http ;
6+ using System . Net . Http . Json ;
67using System . Security . Cryptography . X509Certificates ;
78using System . Threading . Tasks ;
89using GitCredentialManager . Interop . Windows . Native ;
910using Microsoft . Identity . Client ;
1011using Microsoft . Identity . Client . Extensions . Msal ;
1112using System . Text ;
13+ using System . Text . Json ;
1214using System . Threading ;
1315using GitCredentialManager . UI ;
1416using GitCredentialManager . UI . Controls ;
@@ -63,6 +65,14 @@ Task<IMicrosoftAuthenticationResult> GetTokenForUserAsync(string authority, stri
6365 /// - <c>"resource://{guid}"</c> - Use the user-assigned managed identity with resource ID <c>{guid}</c>.
6466 /// </remarks>
6567 Task < IMicrosoftAuthenticationResult > GetTokenForManagedIdentityAsync ( string managedIdentity , string resource ) ;
68+
69+ /// <summary>
70+ /// Acquire a token using workload federation.
71+ /// </summary>
72+ /// <param name="fedOpts">An object containing configuration workload federation.</param>
73+ /// <param name="scopes">Scopes to request.</param>
74+ /// <returns>Authentication result including access token.</returns>
75+ Task < IMicrosoftAuthenticationResult > GetTokenUsingWorkloadFederationAsync ( MicrosoftWorkloadFederationOptions fedOpts , string [ ] scopes ) ;
6676 }
6777
6878 public class ServicePrincipalIdentity
@@ -287,7 +297,8 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsy
287297 }
288298 }
289299
290- public async Task < IMicrosoftAuthenticationResult > GetTokenForManagedIdentityAsync ( string managedIdentity , string resource )
300+ public async Task < IMicrosoftAuthenticationResult > GetTokenForManagedIdentityAsync (
301+ string managedIdentity , string resource )
291302 {
292303 var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor ( Context . HttpClientFactory ) ;
293304
@@ -306,8 +317,88 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForManagedIdentityAsyn
306317 {
307318 Context . Trace . WriteLine ( mid == ManagedIdentityId . SystemAssigned
308319 ? "Failed to acquire token for system managed identity."
309- : $ "Failed to acquire token for user managed identity '{ managedIdentity : D} '.") ;
320+ : $ "Failed to acquire token for user managed identity '{ managedIdentity } '.") ;
321+ Context . Trace . WriteException ( ex ) ;
322+ throw ;
323+ }
324+ }
325+
326+ public async Task < IMicrosoftAuthenticationResult > GetTokenUsingWorkloadFederationAsync ( MicrosoftWorkloadFederationOptions fedOpts , string [ ] scopes )
327+ {
328+ IConfidentialClientApplication app = await CreateConfidentialClientApplicationAsync ( fedOpts ) ;
329+
330+ AuthenticationResult result = await app . AcquireTokenForClient ( scopes )
331+ . ExecuteAsync ( )
332+ . ConfigureAwait ( false ) ;
333+
334+ return new MsalResult ( result ) ;
335+ }
336+
337+ private async Task < string > GetClientAssertion ( MicrosoftWorkloadFederationOptions fedOpts , AssertionRequestOptions _ )
338+ {
339+ switch ( fedOpts . Scenario )
340+ {
341+ case MicrosoftWorkloadFederationScenario . Generic :
342+ Context . Trace . WriteLine ( "Getting client assertion for generic workload federation scenario..." ) ;
343+ if ( string . IsNullOrWhiteSpace ( fedOpts . GenericClientAssertion ) )
344+ throw new InvalidOperationException (
345+ "Client assertion must be provided for generic workload federation scenario." ) ;
346+ return fedOpts . GenericClientAssertion ;
347+
348+ case MicrosoftWorkloadFederationScenario . ManagedIdentity :
349+ Context . Trace . WriteLine ( "Getting client assertion for managed identity workload federation scenario..." ) ;
350+ var miResult = await GetTokenForManagedIdentityAsync ( fedOpts . ManagedIdentityId , fedOpts . Audience ) ;
351+ return miResult . AccessToken ;
352+
353+ case MicrosoftWorkloadFederationScenario . GitHubActions :
354+ Context . Trace . WriteLine ( "Getting client assertion for GitHub Actions workload federation scenario..." ) ;
355+ return await GetGitHubOidcToken ( fedOpts . GitHubTokenRequestUrl , fedOpts . Audience , fedOpts . GitHubTokenRequestToken ) ;
356+
357+ default :
358+ throw new ArgumentOutOfRangeException ( nameof ( fedOpts . Scenario ) , fedOpts . Scenario , "Unsupported workload federation scenario." ) ;
359+ }
360+ }
361+
362+ private async Task < string > GetGitHubOidcToken ( Uri requestUri , string audience , string requestToken )
363+ {
364+ using HttpClient http = Context . HttpClientFactory . CreateClient ( ) ;
365+
366+ UriBuilder ub = new UriBuilder ( requestUri ) ;
367+ if ( ub . Query . Length > 0 ) ub . Query += "&" ;
368+ ub . Query += $ "audience={ Uri . EscapeDataString ( audience ) } ";
369+
370+ using var request = new HttpRequestMessage ( HttpMethod . Get , ub . Uri ) ;
371+ request . AddBearerAuthenticationHeader ( requestToken ) ;
372+
373+ Context . Trace . WriteLine ( $ "Requesting GitHub OIDC token from '{ request . RequestUri } '...") ;
374+ Context . Trace . WriteLineSecrets ( "OIDC request token: {0}" , new [ ] { requestToken } ) ;
375+ using HttpResponseMessage response = await http . SendAsync ( request ) ;
376+ if ( ! response . IsSuccessStatusCode )
377+ {
378+ string error = await response . Content . ReadAsStringAsync ( ) ;
379+ Context . Trace . WriteLine ( $ "Failed to acquire GitHub OIDC token [{ response . StatusCode : D} { response . StatusCode } ]: { error } ") ;
380+ response . EnsureSuccessStatusCode ( ) ;
381+ }
382+
383+ string json = await response . Content . ReadAsStringAsync ( ) ;
384+
385+ try
386+ {
387+ using JsonDocument jsonDoc = JsonDocument . Parse ( json ) ;
388+ if ( ! jsonDoc . RootElement . TryGetProperty ( "value" , out JsonElement tokenElement ) )
389+ {
390+ throw new InvalidOperationException (
391+ "Invalid response from GitHub OIDC token endpoint: 'value' property not found." ) ;
392+ }
393+
394+ return tokenElement . GetString ( ) ??
395+ throw new InvalidOperationException (
396+ "Invalid response from GitHub OIDC token endpoint: 'value' property is null." ) ;
397+ }
398+ catch ( Exception ex )
399+ {
310400 Context . Trace . WriteException ( ex ) ;
401+ Context . Trace . WriteLine ( $ "OIDC token response: { json } ") ;
311402 throw ;
312403 }
313404 }
@@ -558,6 +649,24 @@ private async Task<IConfidentialClientApplication> CreateConfidentialClientAppli
558649 return app ;
559650 }
560651
652+ private async Task < IConfidentialClientApplication > CreateConfidentialClientApplicationAsync (
653+ MicrosoftWorkloadFederationOptions fedOpts )
654+ {
655+ var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor ( Context . HttpClientFactory ) ;
656+
657+ Context . Trace . WriteLine ( $ "Creating federated confidential client application for { fedOpts . TenantId } /{ fedOpts . ClientId } ...") ;
658+ var appBuilder = ConfidentialClientApplicationBuilder . Create ( fedOpts . ClientId )
659+ . WithTenantId ( fedOpts . TenantId )
660+ . WithHttpClientFactory ( httpFactoryAdaptor )
661+ . WithClientAssertion ( reqOpts => GetClientAssertion ( fedOpts , reqOpts ) ) ;
662+
663+ IConfidentialClientApplication app = appBuilder . Build ( ) ;
664+
665+ await RegisterTokenCacheAsync ( app . AppTokenCache , CreateAppTokenCacheProps , Context . Trace2 ) ;
666+
667+ return app ;
668+ }
669+
561670 #endregion
562671
563672 #region Helpers
0 commit comments