From a25aa2d5899666cbaa58bef5cfed226ab72d7606 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 12 Mar 2026 15:55:18 +0000 Subject: [PATCH 1/2] wif: Add implementation of Workload Identity Federation for AzRepos Add support for Workload Identity Federation (WIF) for Azure Repos. This enables users to authenticate to Azure Repos using federated tokens from Managed Identities, GitHub Actions, or generic identity providers. We support three scenarios: 1. Generic When you have a pre-obtained client assertion token from any external identity provider. You provide the assertion directly and GCM exchanges it for an access token. 2. Entra ID Managed Identities When your workload runs on an Azure resource that has a Managed Identity assigned. GCM will first request a token from the Managed Identity for the configured audience, then exchange that token for an Azure DevOps access token. 3. GitHub Actions When your workload runs in a GitHub Actions workflow. GCM will automatically obtain an OIDC token from the GitHub Actions runtime and exchange it for an Azure DevOps access token. Signed-off-by: Matthew John Cheetham --- .../Authentication/MicrosoftAuthentication.cs | 112 +++++++- .../MicrosoftWorkloadFederationOptions.cs | 77 ++++++ src/shared/Core/Constants.cs | 5 + .../AzureReposHostProviderTests.cs | 242 ++++++++++++++++++ .../AzureDevOpsConstants.cs | 12 + .../AzureReposHostProvider.cs | 171 +++++++++++++ 6 files changed, 617 insertions(+), 2 deletions(-) create mode 100644 src/shared/Core/Authentication/MicrosoftWorkloadFederationOptions.cs diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 5e2eca63c..5d65fa982 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using System.Text; +using System.Text.Json; using System.Threading; using GitCredentialManager.UI; using GitCredentialManager.UI.Controls; @@ -63,6 +64,14 @@ Task GetTokenForUserAsync(string authority, stri /// - "resource://{guid}" - Use the user-assigned managed identity with resource ID {guid}. /// Task GetTokenForManagedIdentityAsync(string managedIdentity, string resource); + + /// + /// Acquire a token using workload federation. + /// + /// An object containing configuration workload federation. + /// Scopes to request. + /// Authentication result including access token. + Task GetTokenUsingWorkloadFederationAsync(MicrosoftWorkloadFederationOptions fedOpts, string[] scopes); } public class ServicePrincipalIdentity @@ -287,7 +296,8 @@ public async Task GetTokenForServicePrincipalAsy } } - public async Task GetTokenForManagedIdentityAsync(string managedIdentity, string resource) + public async Task GetTokenForManagedIdentityAsync( + string managedIdentity, string resource) { var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory); @@ -306,8 +316,88 @@ public async Task GetTokenForManagedIdentityAsyn { Context.Trace.WriteLine(mid == ManagedIdentityId.SystemAssigned ? "Failed to acquire token for system managed identity." - : $"Failed to acquire token for user managed identity '{managedIdentity:D}'."); + : $"Failed to acquire token for user managed identity '{managedIdentity}'."); + Context.Trace.WriteException(ex); + throw; + } + } + + public async Task GetTokenUsingWorkloadFederationAsync(MicrosoftWorkloadFederationOptions fedOpts, string[] scopes) + { + IConfidentialClientApplication app = await CreateConfidentialClientApplicationAsync(fedOpts); + + AuthenticationResult result = await app.AcquireTokenForClient(scopes) + .ExecuteAsync() + .ConfigureAwait(false); + + return new MsalResult(result); + } + + private async Task GetClientAssertion(MicrosoftWorkloadFederationOptions fedOpts, AssertionRequestOptions _) + { + switch (fedOpts.Scenario) + { + case MicrosoftWorkloadFederationScenario.Generic: + Context.Trace.WriteLine("Getting client assertion for generic workload federation scenario..."); + if (string.IsNullOrWhiteSpace(fedOpts.GenericClientAssertion)) + throw new InvalidOperationException( + "Client assertion must be provided for generic workload federation scenario."); + return fedOpts.GenericClientAssertion; + + case MicrosoftWorkloadFederationScenario.ManagedIdentity: + Context.Trace.WriteLine("Getting client assertion for managed identity workload federation scenario..."); + var miResult = await GetTokenForManagedIdentityAsync(fedOpts.ManagedIdentityId, fedOpts.Audience); + return miResult.AccessToken; + + case MicrosoftWorkloadFederationScenario.GitHubActions: + Context.Trace.WriteLine("Getting client assertion for GitHub Actions workload federation scenario..."); + return await GetGitHubOidcToken(fedOpts.GitHubTokenRequestUrl, fedOpts.Audience, fedOpts.GitHubTokenRequestToken); + + default: + throw new ArgumentOutOfRangeException(nameof(fedOpts.Scenario), fedOpts.Scenario, "Unsupported workload federation scenario."); + } + } + + private async Task GetGitHubOidcToken(Uri requestUri, string audience, string requestToken) + { + using HttpClient http = Context.HttpClientFactory.CreateClient(); + + UriBuilder ub = new UriBuilder(requestUri); + if (ub.Query.Length > 0) ub.Query += "&"; + ub.Query += $"audience={Uri.EscapeDataString(audience)}"; + + using var request = new HttpRequestMessage(HttpMethod.Get, ub.Uri); + request.AddBearerAuthenticationHeader(requestToken); + + Context.Trace.WriteLine($"Requesting GitHub OIDC token from '{request.RequestUri}'..."); + Context.Trace.WriteLineSecrets("OIDC request token: {0}", new[] { requestToken }); + using HttpResponseMessage response = await http.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + string error = await response.Content.ReadAsStringAsync(); + Context.Trace.WriteLine($"Failed to acquire GitHub OIDC token [{response.StatusCode:D} {response.StatusCode}]: {error}"); + response.EnsureSuccessStatusCode(); + } + + string json = await response.Content.ReadAsStringAsync(); + + try + { + using JsonDocument jsonDoc = JsonDocument.Parse(json); + if (!jsonDoc.RootElement.TryGetProperty("value", out JsonElement tokenElement)) + { + throw new InvalidOperationException( + "Invalid response from GitHub OIDC token endpoint: 'value' property not found."); + } + + return tokenElement.GetString() ?? + throw new InvalidOperationException( + "Invalid response from GitHub OIDC token endpoint: 'value' property is null."); + } + catch (Exception ex) + { Context.Trace.WriteException(ex); + Context.Trace.WriteLine($"OIDC token response: {json}"); throw; } } @@ -558,6 +648,24 @@ private async Task CreateConfidentialClientAppli return app; } + private async Task CreateConfidentialClientApplicationAsync( + MicrosoftWorkloadFederationOptions fedOpts) + { + var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory); + + Context.Trace.WriteLine($"Creating federated confidential client application for {fedOpts.TenantId}/{fedOpts.ClientId}..."); + var appBuilder = ConfidentialClientApplicationBuilder.Create(fedOpts.ClientId) + .WithTenantId(fedOpts.TenantId) + .WithHttpClientFactory(httpFactoryAdaptor) + .WithClientAssertion(reqOpts => GetClientAssertion(fedOpts, reqOpts)); + + IConfidentialClientApplication app = appBuilder.Build(); + + await RegisterTokenCacheAsync(app.AppTokenCache, CreateAppTokenCacheProps, Context.Trace2); + + return app; + } + #endregion #region Helpers diff --git a/src/shared/Core/Authentication/MicrosoftWorkloadFederationOptions.cs b/src/shared/Core/Authentication/MicrosoftWorkloadFederationOptions.cs new file mode 100644 index 000000000..5511c0dee --- /dev/null +++ b/src/shared/Core/Authentication/MicrosoftWorkloadFederationOptions.cs @@ -0,0 +1,77 @@ +using System; + +namespace GitCredentialManager.Authentication; + +public enum MicrosoftWorkloadFederationScenario +{ + /// + /// Federate via pre-computed client assertion. + /// + Generic, + + /// + /// Federate via an access token for an Entra ID Managed Identity. + /// + ManagedIdentity, + + /// + /// Federate via a GitHub Actions OIDC token. + /// + GitHubActions, +} + +public class MicrosoftWorkloadFederationOptions +{ + public const string DefaultAudience = Constants.DefaultWorkloadFederationAudience; + + private string _audience = DefaultAudience; + + /// + /// The workload federation scenario to use. + /// + public MicrosoftWorkloadFederationScenario Scenario { get; set; } + + /// + /// Tenant ID of the identity to request an access token for. + /// + public string TenantId { get; set; } + + /// + /// Client ID of the identity to request an access token for. + /// + public string ClientId { get; set; } + + /// + /// The audience to use when requesting a token. + /// + /// If this is null, the default audience will be used. + public string Audience + { + get => _audience; + set => _audience = value ?? DefaultAudience; + } + + /// + /// Generic assertion. + /// + /// Used with the federation scenario. + public string GenericClientAssertion { get; set; } + + /// + /// The managed identity to request a federated token for, to exchange for an access token. + /// + /// Used with the federation scenario. + public string ManagedIdentityId { get; set; } + + /// + /// GitHub Actions OIDC token request URI. + /// + /// Used with the federation scenario. + public Uri GitHubTokenRequestUrl { get; set; } + + /// + /// GitHub Actions OIDC token request token. + /// + /// Used with the federation scenario. + public string GitHubTokenRequestToken { get; set; } +} diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index e6625a6ea..6fecc2b38 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -31,6 +31,8 @@ public static class Constants /// public static readonly Guid MsaTransferTenantId = new("f8cdef31-a31e-4b4a-93e4-5f571e91255a"); + public const string DefaultWorkloadFederationAudience = "api://AzureADTokenExchange"; + public static class CredentialProtocol { public const string NtlmKey = "ntlm"; @@ -130,6 +132,9 @@ public static class EnvironmentVariables public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS"; public const string GcmGuiSoftwareRendering = "GCM_GUI_SOFTWARE_RENDERING"; public const string GcmAllowUnsafeRemotes = "GCM_ALLOW_UNSAFE_REMOTES"; + + public const string GitHubActionsTokenRequestUrl = "ACTIONS_ID_TOKEN_REQUEST_URL"; + public const string GitHubActionsTokenRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN"; } public static class Http diff --git a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs index bfd14d14f..e05db1646 100644 --- a/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs +++ b/src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs @@ -605,6 +605,248 @@ public async Task AzureReposProvider_GetCredentialAsync_ManagedIdentity_ReturnsM AzureDevOpsConstants.AzureDevOpsResourceId), Times.Once); } + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_WorkloadFederation_Generic_ReturnsFederationOptions() + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["path"] = "org/proj/_git/repo" + }); + + const string accessToken = "FEDERATED-IDENTITY-TOKEN"; + const string wifScenario = "generic"; + const string tenantId = "00000000-0000-0000-0000-000000000000"; + const string clientId = "11111111-1111-1111-1111-111111111111"; + const string assertion = "CLIENT-ASSERTION"; + + var context = new TestCommandContext + { + Environment = + { + Variables = + { + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederation] = wifScenario, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationTenantId] = tenantId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationClientId] = clientId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationAssertion] = assertion, + } + } + }; + + var azDevOps = Mock.Of(); + var authorityCache = Mock.Of(); + var userMgr = Mock.Of(); + var msAuthMock = new Mock(); + + msAuthMock.Setup(x => x.GetTokenUsingWorkloadFederationAsync( + It.IsAny(), It.IsAny())) + .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken }); + + var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr); + + GetCredentialResult result = await provider.GetCredentialAsync(input); + ICredential credential = result.Credential; + + Assert.NotNull(credential); + Assert.Equal(clientId, credential.Account); + Assert.Equal(accessToken, credential.Password); + + msAuthMock.Verify( + x => x.GetTokenUsingWorkloadFederationAsync( + It.Is( + fed => fed.Scenario == MicrosoftWorkloadFederationScenario.Generic && + fed.TenantId == tenantId && + fed.ClientId == clientId && + fed.Audience == MicrosoftWorkloadFederationOptions.DefaultAudience && + fed.GenericClientAssertion == assertion), + AzureDevOpsConstants.AzureDevOpsDefaultScopes), Times.Once); + } + + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_WorkloadFederation_GenericFileAssertion_ReadsFromFile() + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["path"] = "org/proj/_git/repo" + }); + + const string accessToken = "FEDERATED-IDENTITY-TOKEN"; + const string wifScenario = "generic"; + const string tenantId = "00000000-0000-0000-0000-000000000000"; + const string clientId = "11111111-1111-1111-1111-111111111111"; + const string assertion = "CLIENT-ASSERTION-FROM-FILE"; + const string filePath = "/tmp/assertion-token.txt"; + + var context = new TestCommandContext + { + Environment = + { + Variables = + { + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederation] = wifScenario, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationTenantId] = tenantId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationClientId] = clientId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationAssertion] = $"file://{filePath}", + } + } + }; + + context.FileSystem.Files[filePath] = System.Text.Encoding.UTF8.GetBytes(assertion); + + var azDevOps = Mock.Of(); + var authorityCache = Mock.Of(); + var userMgr = Mock.Of(); + var msAuthMock = new Mock(); + + msAuthMock.Setup(x => x.GetTokenUsingWorkloadFederationAsync( + It.IsAny(), It.IsAny())) + .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken }); + + var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr); + + GetCredentialResult result = await provider.GetCredentialAsync(input); + ICredential credential = result.Credential; + + Assert.NotNull(credential); + Assert.Equal(clientId, credential.Account); + Assert.Equal(accessToken, credential.Password); + + msAuthMock.Verify( + x => x.GetTokenUsingWorkloadFederationAsync( + It.Is( + fed => fed.Scenario == MicrosoftWorkloadFederationScenario.Generic && + fed.TenantId == tenantId && + fed.ClientId == clientId && + fed.Audience == MicrosoftWorkloadFederationOptions.DefaultAudience && + fed.GenericClientAssertion == assertion), + AzureDevOpsConstants.AzureDevOpsDefaultScopes), Times.Once); + } + + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_WorkloadFederation_MI_ReturnsFederationOptions() + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["path"] = "org/proj/_git/repo" + }); + + const string accessToken = "FEDERATED-IDENTITY-TOKEN"; + const string wifScenario = "managedidentity"; + const string tenantId = "00000000-0000-0000-0000-000000000000"; + const string clientId = "11111111-1111-1111-1111-111111111111"; + const string managedIdentity = "22222222-2222-2222-2222-222222222222"; + + var context = new TestCommandContext + { + Environment = + { + Variables = + { + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederation] = wifScenario, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationTenantId] = tenantId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationClientId] = clientId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationManagedIdentity] = managedIdentity, + } + } + }; + + var azDevOps = Mock.Of(); + var authorityCache = Mock.Of(); + var userMgr = Mock.Of(); + var msAuthMock = new Mock(); + + msAuthMock.Setup(x => x.GetTokenUsingWorkloadFederationAsync( + It.IsAny(), It.IsAny())) + .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken }); + + var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr); + + GetCredentialResult result = await provider.GetCredentialAsync(input); + ICredential credential = result.Credential; + + Assert.NotNull(credential); + Assert.Equal(clientId, credential.Account); + Assert.Equal(accessToken, credential.Password); + + msAuthMock.Verify( + x => x.GetTokenUsingWorkloadFederationAsync( + It.Is( + fed => fed.Scenario == MicrosoftWorkloadFederationScenario.ManagedIdentity && + fed.TenantId == tenantId && + fed.ClientId == clientId && + fed.Audience == MicrosoftWorkloadFederationOptions.DefaultAudience && + fed.ManagedIdentityId == managedIdentity), + AzureDevOpsConstants.AzureDevOpsDefaultScopes), Times.Once); + } + + [Fact] + public async Task AzureReposProvider_GetCredentialAsync_WorkloadFederation_GitHubActions_ReturnsFederationOptions() + { + var input = new InputArguments(new Dictionary + { + ["protocol"] = "https", + ["host"] = "dev.azure.com", + ["path"] = "org/proj/_git/repo" + }); + + const string accessToken = "FEDERATED-IDENTITY-TOKEN"; + const string wifScenario = "githubactions"; + const string tenantId = "00000000-0000-0000-0000-000000000000"; + const string clientId = "11111111-1111-1111-1111-111111111111"; + const string ghRequestUrl = "https://token.actions.example.com/oidc/example?param=value"; + const string ghRequestToken = "OIDC-TOKEN"; + + var context = new TestCommandContext + { + Environment = + { + Variables = + { + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederation] = wifScenario, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationTenantId] = tenantId, + [AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationClientId] = clientId, + [Constants.EnvironmentVariables.GitHubActionsTokenRequestUrl] = ghRequestUrl, + [Constants.EnvironmentVariables.GitHubActionsTokenRequestToken] = ghRequestToken, + } + } + }; + + var azDevOps = Mock.Of(); + var authorityCache = Mock.Of(); + var userMgr = Mock.Of(); + var msAuthMock = new Mock(); + + msAuthMock.Setup(x => x.GetTokenUsingWorkloadFederationAsync( + It.IsAny(), It.IsAny())) + .ReturnsAsync(new MockMsAuthResult { AccessToken = accessToken }); + + var provider = new AzureReposHostProvider(context, azDevOps, msAuthMock.Object, authorityCache, userMgr); + + GetCredentialResult result = await provider.GetCredentialAsync(input); + ICredential credential = result.Credential; + + Assert.NotNull(credential); + Assert.Equal(clientId, credential.Account); + Assert.Equal(accessToken, credential.Password); + + msAuthMock.Verify( + x => x.GetTokenUsingWorkloadFederationAsync( + It.Is( + fed => fed.Scenario == MicrosoftWorkloadFederationScenario.GitHubActions && + fed.TenantId == tenantId && + fed.ClientId == clientId && + fed.GitHubTokenRequestUrl == new Uri(ghRequestUrl) && + fed.GitHubTokenRequestToken == ghRequestToken && + fed.Audience == MicrosoftWorkloadFederationOptions.DefaultAudience), + AzureDevOpsConstants.AzureDevOpsDefaultScopes), Times.Once); + } + [Fact] public async Task AzureReposProvider_GetCredentialAsync_ServicePrincipal_ReturnsSPCredential() { diff --git a/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs b/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs index a282d4eff..f9ec8ce0f 100644 --- a/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs +++ b/src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs @@ -46,6 +46,12 @@ public static class EnvironmentVariables public const string ServicePrincipalCertificateThumbprint = "GCM_AZREPOS_SP_CERT_THUMBPRINT"; public const string ServicePrincipalCertificateSendX5C = "GCM_AZREPOS_SP_CERT_SEND_X5C"; public const string ManagedIdentity = "GCM_AZREPOS_MANAGEDIDENTITY"; + public const string WorkloadFederation = "GCM_AZREPOS_WIF"; + public const string WorkloadFederationClientId = "GCM_AZREPOS_WIF_CLIENTID"; + public const string WorkloadFederationTenantId = "GCM_AZREPOS_WIF_TENANTID"; + public const string WorkloadFederationAudience = "GCM_AZREPOS_WIF_AUDIENCE"; + public const string WorkloadFederationAssertion = "GCM_AZREPOS_WIF_ASSERTION"; + public const string WorkloadFederationManagedIdentity = "GCM_AZREPOS_WIF_MANAGEDIDENTITY"; } public static class GitConfiguration @@ -62,6 +68,12 @@ public static class Credential public const string ServicePrincipalCertificateThumbprint = "azreposServicePrincipalCertificateThumbprint"; public const string ServicePrincipalCertificateSendX5C = "azreposServicePrincipalCertificateSendX5C"; public const string ManagedIdentity = "azreposManagedIdentity"; + public const string WorkloadFederation = "azreposWorkloadFederation"; + public const string WorkloadFederationClientId = "azreposWorkloadFederationClientId"; + public const string WorkloadFederationTenantId = "azreposWorkloadFederationTenantId"; + public const string WorkloadFederationAudience = "azreposWorkloadFederationAudience"; + public const string WorkloadFederationAssertion = "azreposWorkloadFederationAssertion"; + public const string WorkloadFederationManagedIdentity = "azreposWorkloadFederationManagedIdentity"; } } } diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 72eb378eb..9a916a236 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -85,6 +85,15 @@ public async Task GetCredentialAsync(InputArguments input) ); } + if (UseWorkloadFederation(out MicrosoftWorkloadFederationOptions fedOpts)) + { + _context.Trace.WriteLine($"Getting Azure Access Token using WIF (scenario: {fedOpts.Scenario})..."); + var azureResult = await _msAuth.GetTokenUsingWorkloadFederationAsync(fedOpts, AzureDevOpsConstants.AzureDevOpsDefaultScopes); + return new GetCredentialResult( + new GitCredential(fedOpts.ClientId, azureResult.AccessToken) + ); + } + if (UseServicePrincipal(out ServicePrincipalIdentity sp)) { _context.Trace.WriteLine($"Getting Azure Access Token for service principal {sp.TenantId}/{sp.Id}..."); @@ -137,6 +146,10 @@ public Task StoreCredentialAsync(InputArguments input) { _context.Trace.WriteLine("Nothing to store for managed identity authentication."); } + else if (UseWorkloadFederation(out _)) + { + _context.Trace.WriteLine("Nothing to store for federated identity authentication."); + } else if (UseServicePrincipal(out _)) { _context.Trace.WriteLine("Nothing to store for service principal authentication."); @@ -172,6 +185,10 @@ public Task EraseCredentialAsync(InputArguments input) { _context.Trace.WriteLine("Nothing to erase for managed identity authentication."); } + else if (UseWorkloadFederation(out _)) + { + _context.Trace.WriteLine("Nothing to erase for federated identity authentication."); + } else if (UseServicePrincipal(out _)) { _context.Trace.WriteLine("Nothing to erase for service principal authentication."); @@ -588,6 +605,160 @@ private bool UseManagedIdentity(out string mid) !string.IsNullOrWhiteSpace(mid); } + private bool UseWorkloadFederation(out MicrosoftWorkloadFederationOptions fedOpts) + { + if (!_context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.WorkloadFederation, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.WorkloadFederation, + out string wifStr)) + { + fedOpts = null; + return false; + } + + MicrosoftWorkloadFederationScenario scenario; + switch (wifStr.ToLowerInvariant()) + { + case "generic": + scenario = MicrosoftWorkloadFederationScenario.Generic; + break; + + case "mi": + case "managedidentity": + scenario = MicrosoftWorkloadFederationScenario.ManagedIdentity; + break; + + case "github": + case "githubactions": + scenario = MicrosoftWorkloadFederationScenario.GitHubActions; + break; + + default: // Unknown scenario value + fedOpts = null; + return false; + } + + bool hasClientId = _context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationClientId, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.WorkloadFederationClientId, + out string clientId); + + bool hasTenantId = _context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationTenantId, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.WorkloadFederationTenantId, + out string tenantId); + + if (!hasClientId || !hasTenantId) + { + _context.Streams.Error.WriteLine("error: both client ID and tenant ID are required for workload federation"); + fedOpts = null; + return false; + } + + // Audience is optional - the default is "api://AzureADTokenExchange" + if (!_context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationAudience, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.WorkloadFederationAudience, + out string audience) || string.IsNullOrWhiteSpace(audience)) + { + audience = MicrosoftWorkloadFederationOptions.DefaultAudience; + } + + fedOpts = new MicrosoftWorkloadFederationOptions + { + Scenario = scenario, + ClientId = clientId, + TenantId = tenantId, + Audience = audience + }; + + switch (scenario) + { + case MicrosoftWorkloadFederationScenario.Generic: + if (!_context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationAssertion, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.WorkloadFederationAssertion, + out string assertion) || string.IsNullOrWhiteSpace(assertion)) + { + _context.Streams.Error.WriteLine("error: assertion is required for the generic workload federation scenario"); + fedOpts = null; + return false; + } + + // Check if this value points to a file containing the actual assertion (file://) + if (Uri.TryCreate(assertion, UriKind.Absolute, out Uri assertionUri) + && StringComparer.OrdinalIgnoreCase.Equals(assertionUri.Scheme, "file")) + { + string filePath = assertionUri.LocalPath; + if (!_context.FileSystem.FileExists(filePath)) + { + _context.Streams.Error.WriteLine($"error: assertion file not found: {filePath}"); + fedOpts = null; + return false; + } + + _context.Trace.WriteLine($"Reading workload federation assertion from file '{filePath}'..."); + assertion = _context.FileSystem.ReadAllText(filePath).Trim(); + if (string.IsNullOrWhiteSpace(assertion)) + { + _context.Streams.Error.WriteLine($"error: assertion file is empty: {filePath}"); + fedOpts = null; + return false; + } + } + + fedOpts.GenericClientAssertion = assertion; + break; + + case MicrosoftWorkloadFederationScenario.ManagedIdentity: + if (!_context.Settings.TryGetSetting( + AzureDevOpsConstants.EnvironmentVariables.WorkloadFederationManagedIdentity, + Constants.GitConfiguration.Credential.SectionName, + AzureDevOpsConstants.GitConfiguration.Credential.WorkloadFederationManagedIdentity, + out string managedIdentity) || string.IsNullOrWhiteSpace(managedIdentity)) + { + _context.Streams.Error.WriteLine("error: managed identity is required for the managed identity workload federation scenario"); + fedOpts = null; + return false; + } + + fedOpts.ManagedIdentityId = managedIdentity; + break; + + case MicrosoftWorkloadFederationScenario.GitHubActions: + if (!_context.Environment.Variables.TryGetValue( + Constants.EnvironmentVariables.GitHubActionsTokenRequestUrl, out string tokenRequestUrl) + || !Uri.TryCreate(tokenRequestUrl, UriKind.Absolute, out Uri tokenRequestUri)) + { + _context.Streams.Error.WriteLine( + "error: unable to get valid token request URL from environment variable for the GitHub Actions workload federation scenario"); + fedOpts = null; + return false; + } + + if (!_context.Environment.Variables.TryGetValue( + Constants.EnvironmentVariables.GitHubActionsTokenRequestToken, out string tokenRequestToken) + || string.IsNullOrWhiteSpace(tokenRequestToken)) + { + _context.Streams.Error.WriteLine( + "error: unable to get valid token request token from environment variable for the GitHub Actions workload federation scenario"); + fedOpts = null; + return false; + } + + fedOpts.GitHubTokenRequestUrl = tokenRequestUri; + fedOpts.GitHubTokenRequestToken = tokenRequestToken; + break; + } + + return true; + } + #endregion #region IConfigurationComponent From 9c38f6a9698be22983eba2713f66c8e9bef8d893 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 12 Mar 2026 15:55:43 +0000 Subject: [PATCH 2/2] wif: add documentation for WIF Signed-off-by: Matthew John Cheetham --- docs/azrepos-wif.md | 185 ++++++++++++++++++++++++++++++++++++++++++ docs/configuration.md | 146 +++++++++++++++++++++++++++++++++ docs/environment.md | 180 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 511 insertions(+) create mode 100644 docs/azrepos-wif.md diff --git a/docs/azrepos-wif.md b/docs/azrepos-wif.md new file mode 100644 index 000000000..0e8f0147e --- /dev/null +++ b/docs/azrepos-wif.md @@ -0,0 +1,185 @@ +# Azure Workload Identity Federation + +Git Credential Manager supports [Workload Identity Federation][wif] for +authentication with Azure Repos. This document provides an overview of Workload +Identity Federation and how to use it with GCM. + +## Overview + +Workload Identity Federation allows a workload (such as a CI/CD pipeline, VM, or +container) to exchange a token from an external identity provider for a Microsoft +Entra ID access token — without needing to manage secrets like client secrets or +certificates. + +This is especially useful in scenarios where: + +- You want to avoid storing long-lived secrets. +- Your workload already has an identity token from another provider (e.g., GitHub + Actions OIDC, a Managed Identity, or a custom identity provider). +- You want to follow the principle of least privilege with short-lived, + automatically rotated credentials. + +You can read more about Workload Identity Federation in the +[Microsoft Entra documentation][wif]. + +## How it works + +When configured, GCM obtains a client assertion (a token from the external +identity provider) and exchanges it with Microsoft Entra ID for an access token +scoped to Azure DevOps. The exact mechanism for obtaining the client assertion +depends on the federation scenario you choose. + +## Scenarios + +GCM supports three federation scenarios: + +### Generic + +Use this scenario when you have a pre-obtained client assertion token from any +external identity provider. You provide the assertion directly and GCM exchanges +it for an access token. + +**Required settings:** + +Setting|Git Configuration|Environment Variable +-|-|- +Scenario|[`credential.azreposWorkloadFederation`][gcm-wif-config]|[`GCM_AZREPOS_WIF`][gcm-wif-env] +Client ID|[`credential.azreposWorkloadFederationClientId`][gcm-wif-clientid-config]|[`GCM_AZREPOS_WIF_CLIENTID`][gcm-wif-clientid-env] +Tenant ID|[`credential.azreposWorkloadFederationTenantId`][gcm-wif-tenantid-config]|[`GCM_AZREPOS_WIF_TENANTID`][gcm-wif-tenantid-env] +Assertion|[`credential.azreposWorkloadFederationAssertion`][gcm-wif-assertion-config]|[`GCM_AZREPOS_WIF_ASSERTION`][gcm-wif-assertion-env] + +**Optional settings:** + +Setting|Git Configuration|Environment Variable +-|-|- +Audience|[`credential.azreposWorkloadFederationAudience`][gcm-wif-audience-config]|[`GCM_AZREPOS_WIF_AUDIENCE`][gcm-wif-audience-env] + +#### Example + +```shell +git config --global credential.azreposWorkloadFederation generic +git config --global credential.azreposWorkloadFederationClientId "11111111-1111-1111-1111-111111111111" +git config --global credential.azreposWorkloadFederationTenantId "22222222-2222-2222-2222-222222222222" +git config --global credential.azreposWorkloadFederationAssertion "eyJhbGci..." +``` + +### Managed Identity + +Use this scenario when your workload runs on an Azure resource that has a +[Managed Identity][az-mi] assigned. GCM will first request a token from the +Managed Identity for the configured audience, then exchange that token for an +Azure DevOps access token. + +This is useful for Azure VMs, App Services, or other Azure resources that have a +Managed Identity but need to authenticate as a specific app registration with +a federated credential trust. + +**Required settings:** + +Setting|Git Configuration|Environment Variable +-|-|- +Scenario|[`credential.azreposWorkloadFederation`][gcm-wif-config]|[`GCM_AZREPOS_WIF`][gcm-wif-env] +Client ID|[`credential.azreposWorkloadFederationClientId`][gcm-wif-clientid-config]|[`GCM_AZREPOS_WIF_CLIENTID`][gcm-wif-clientid-env] +Tenant ID|[`credential.azreposWorkloadFederationTenantId`][gcm-wif-tenantid-config]|[`GCM_AZREPOS_WIF_TENANTID`][gcm-wif-tenantid-env] +Managed Identity|[`credential.azreposWorkloadFederationManagedIdentity`][gcm-wif-mi-config]|[`GCM_AZREPOS_WIF_MANAGEDIDENTITY`][gcm-wif-mi-env] + +**Optional settings:** + +Setting|Git Configuration|Environment Variable +-|-|- +Audience|[`credential.azreposWorkloadFederationAudience`][gcm-wif-audience-config]|[`GCM_AZREPOS_WIF_AUDIENCE`][gcm-wif-audience-env] + +The Managed Identity value accepts the same formats as +[`credential.azreposManagedIdentity`][gcm-mi-config]: + +Value|Description +-|- +`system`|System-Assigned Managed Identity +`[guid]`|User-Assigned Managed Identity with the specified client ID +`id://[guid]`|User-Assigned Managed Identity with the specified client ID +`resource://[guid]`|User-Assigned Managed Identity for the associated resource + +#### Example + +```shell +git config --global credential.azreposWorkloadFederation managedidentity +git config --global credential.azreposWorkloadFederationClientId "11111111-1111-1111-1111-111111111111" +git config --global credential.azreposWorkloadFederationTenantId "22222222-2222-2222-2222-222222222222" +git config --global credential.azreposWorkloadFederationManagedIdentity system +``` + +### GitHub Actions + +Use this scenario when your workload runs in a GitHub Actions workflow. GCM will +automatically obtain an OIDC token from the GitHub Actions runtime and exchange +it for an Azure DevOps access token. + +This scenario uses the `ACTIONS_ID_TOKEN_REQUEST_URL` and +`ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variables that GitHub Actions +automatically provides when a workflow has the `id-token: write` permission. + +**Required settings:** + +Setting|Git Configuration|Environment Variable +-|-|- +Scenario|[`credential.azreposWorkloadFederation`][gcm-wif-config]|[`GCM_AZREPOS_WIF`][gcm-wif-env] +Client ID|[`credential.azreposWorkloadFederationClientId`][gcm-wif-clientid-config]|[`GCM_AZREPOS_WIF_CLIENTID`][gcm-wif-clientid-env] +Tenant ID|[`credential.azreposWorkloadFederationTenantId`][gcm-wif-tenantid-config]|[`GCM_AZREPOS_WIF_TENANTID`][gcm-wif-tenantid-env] + +**Optional settings:** + +Setting|Git Configuration|Environment Variable +-|-|- +Audience|[`credential.azreposWorkloadFederationAudience`][gcm-wif-audience-config]|[`GCM_AZREPOS_WIF_AUDIENCE`][gcm-wif-audience-env] + +No additional GCM settings are required — the GitHub Actions OIDC environment +variables are read automatically. + +#### Prerequisites + +1. An app registration in Microsoft Entra ID with a federated credential + configured to trust your GitHub repository. +2. The app registration must have the necessary permissions to access Azure + DevOps. +3. Your GitHub Actions workflow must have the `id-token: write` permission. + +#### Example workflow + +```yaml +permissions: + id-token: write + contents: read + +steps: + - uses: actions/checkout@v4 + env: + GCM_AZREPOS_WIF: githubactions + GCM_AZREPOS_WIF_CLIENTID: "11111111-1111-1111-1111-111111111111" + GCM_AZREPOS_WIF_TENANTID: "22222222-2222-2222-2222-222222222222" +``` + +## Audience + +All scenarios accept an optional audience setting that controls the audience +claim in the federated token request. The default value is +`api://AzureADTokenExchange`, which is the standard audience for Microsoft Entra +ID workload identity federation. + +You only need to change this if your federated credential trust is configured +with a custom audience. + +[az-mi]: https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview +[wif]: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation +[gcm-mi-config]: https://gh.io/gcm/config#credentialazreposmanagedidentity +[gcm-wif-config]: https://gh.io/gcm/config#credentialazreposworkloadfederation +[gcm-wif-clientid-config]: https://gh.io/gcm/config#credentialazreposworkloadfederationclientid +[gcm-wif-tenantid-config]: https://gh.io/gcm/config#credentialazreposworkloadfederationtenantid +[gcm-wif-audience-config]: https://gh.io/gcm/config#credentialazreposworkloadfederationaudience +[gcm-wif-assertion-config]: https://gh.io/gcm/config#credentialazreposworkloadfederationassertion +[gcm-wif-mi-config]: https://gh.io/gcm/config#credentialazreposworkloadfederationmanagedidentity +[gcm-wif-env]: https://gh.io/gcm/env#GCM_AZREPOS_WIF +[gcm-wif-clientid-env]: https://gh.io/gcm/env#GCM_AZREPOS_WIF_CLIENTID +[gcm-wif-tenantid-env]: https://gh.io/gcm/env#GCM_AZREPOS_WIF_TENANTID +[gcm-wif-audience-env]: https://gh.io/gcm/env#GCM_AZREPOS_WIF_AUDIENCE +[gcm-wif-assertion-env]: https://gh.io/gcm/env#GCM_AZREPOS_WIF_ASSERTION +[gcm-wif-mi-env]: https://gh.io/gcm/env#GCM_AZREPOS_WIF_MANAGEDIDENTITY diff --git a/docs/configuration.md b/docs/configuration.md index ba978ef30..af5d410f4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -884,6 +884,138 @@ git config --global credential.azreposManagedIdentity "id://11111111-1111-1111-1 --- +### credential.azreposWorkloadFederation + +Use [Workload Identity Federation][wif] to authenticate with Azure Repos. + +The value specifies the federation scenario to use for obtaining a client +assertion to exchange for an access token. + +You must also set the following companion settings: + +- [credential.azreposWorkloadFederationClientId][credential-azrepos-wif-clientid] +- [credential.azreposWorkloadFederationTenantId][credential-azrepos-wif-tenantid] + +Depending on the scenario, additional settings may be required. + +Value|Description +-|- +`generic`|Use a user-supplied client assertion ([credential.azreposWorkloadFederationAssertion][credential-azrepos-wif-assertion]) +`managedidentity`|Use a [Managed Identity][managed-identity] to obtain the federated token ([credential.azreposWorkloadFederationManagedIdentity][credential-azrepos-wif-managedidentity]) +`githubactions`|Automatically obtain an OIDC token from GitHub Actions + +For more information about workload identity federation, see the +[conceptual documentation][azrepos-wif-doc] and the Azure DevOps +[documentation][azrepos-sp-mid]. + +#### Example + +```shell +git config --global credential.azreposWorkloadFederation githubactions +``` + +**Also see: [GCM_AZREPOS_WIF][gcm-azrepos-wif]** + +--- + +### credential.azreposWorkloadFederationClientId + +The client ID of the app registration / service principal to request an access +token for when using [Workload Identity Federation][wif] with +[credential.azreposWorkloadFederation][credential-azrepos-wif]. + +#### Example + +```shell +git config --global credential.azreposWorkloadFederationClientId "11111111-1111-1111-1111-111111111111" +``` + +**Also see: [GCM_AZREPOS_WIF_CLIENTID][gcm-azrepos-wif-clientid]** + +--- + +### credential.azreposWorkloadFederationTenantId + +The tenant ID of the app registration / service principal to request an access +token for when using [Workload Identity Federation][wif] with +[credential.azreposWorkloadFederation][credential-azrepos-wif]. + +#### Example + +```shell +git config --global credential.azreposWorkloadFederationTenantId "22222222-2222-2222-2222-222222222222" +``` + +**Also see: [GCM_AZREPOS_WIF_TENANTID][gcm-azrepos-wif-tenantid]** + +--- + +### credential.azreposWorkloadFederationAudience + +The audience to use when requesting the federated token for +[Workload Identity Federation][wif] with +[credential.azreposWorkloadFederation][credential-azrepos-wif]. + +Defaults to `api://AzureADTokenExchange`. + +#### Example + +```shell +git config --global credential.azreposWorkloadFederationAudience "api://AzureADTokenExchange" +``` + +**Also see: [GCM_AZREPOS_WIF_AUDIENCE][gcm-azrepos-wif-audience]** + +--- + +### credential.azreposWorkloadFederationAssertion + +Specifies the client assertion token to use with the `generic` +[Workload Identity Federation][wif] scenario +([credential.azreposWorkloadFederation][credential-azrepos-wif]). + +This setting is required when `credential.azreposWorkloadFederation` is set to +`generic`. + +#### Example + +```shell +git config --global credential.azreposWorkloadFederationAssertion "eyJhbGci..." +``` + +**Also see: [GCM_AZREPOS_WIF_ASSERTION][gcm-azrepos-wif-assertion]** + +--- + +### credential.azreposWorkloadFederationManagedIdentity + +Specifies the [Managed Identity][managed-identity] to use to obtain a federated +token for the `managedidentity` [Workload Identity Federation][wif] scenario +([credential.azreposWorkloadFederation][credential-azrepos-wif]). + +This setting is required when `credential.azreposWorkloadFederation` is set to +`managedidentity`. + +The value accepts the same formats as +[credential.azreposManagedIdentity](#credentialazreposmanagedidentity). + +Value|Description +-|- +`system`|System-Assigned Managed Identity +`[guid]`|User-Assigned Managed Identity with the specified client ID +`id://[guid]`|User-Assigned Managed Identity with the specified client ID +`resource://[guid]`|User-Assigned Managed Identity for the associated resource + +#### Example + +```shell +git config --global credential.azreposWorkloadFederationManagedIdentity system +``` + +**Also see: [GCM_AZREPOS_WIF_MANAGEDIDENTITY][gcm-azrepos-wif-managedidentity]** + +--- + ### credential.azreposServicePrincipal Specify the client and tenant IDs of a [service principal][service-principal] @@ -1048,6 +1180,12 @@ Defaults to disabled. [gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT [gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE [gcm-azrepos-credentialmanagedidentity]: environment.md#GCM_AZREPOS_MANAGEDIDENTITY +[gcm-azrepos-wif]: environment.md#GCM_AZREPOS_WIF +[gcm-azrepos-wif-clientid]: environment.md#GCM_AZREPOS_WIF_CLIENTID +[gcm-azrepos-wif-tenantid]: environment.md#GCM_AZREPOS_WIF_TENANTID +[gcm-azrepos-wif-audience]: environment.md#GCM_AZREPOS_WIF_AUDIENCE +[gcm-azrepos-wif-assertion]: environment.md#GCM_AZREPOS_WIF_ASSERTION +[gcm-azrepos-wif-managedidentity]: environment.md#GCM_AZREPOS_WIF_MANAGEDIDENTITY [gcm-bitbucket-always-refresh-credentials]: environment.md#GCM_BITBUCKET_ALWAYS_REFRESH_CREDENTIALS [gcm-bitbucket-authmodes]: environment.md#GCM_BITBUCKET_AUTHMODES [gcm-credential-cache-options]: environment.md#GCM_CREDENTIAL_CACHE_OPTIONS @@ -1077,6 +1215,7 @@ Defaults to disabled. [autodetect]: autodetect.md [libsecret]: https://wiki.gnome.org/Projects/Libsecret [managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview +[wif]: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation [provider-migrate]: migration.md#gcm_authority [cache-options]: https://git-scm.com/docs/git-credential-cache#_options [pass]: https://www.passwordstore.org/ @@ -1090,6 +1229,13 @@ Defaults to disabled. [wam]: windows-broker.md [service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals [azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity +[azrepos-wif-doc]: azrepos-wif.md +[credential-azrepos-wif]: #credentialazreposworkloadfederation +[credential-azrepos-wif-clientid]: #credentialazreposworkloadfederationclientid +[credential-azrepos-wif-tenantid]: #credentialazreposworkloadfederationtenantid +[credential-azrepos-wif-audience]: #credentialazreposworkloadfederationaudience +[credential-azrepos-wif-assertion]: #credentialazreposworkloadfederationassertion +[credential-azrepos-wif-managedidentity]: #credentialazreposworkloadfederationmanagedidentity [credential-azrepos-sp]: #credentialazreposserviceprincipal [credential-azrepos-sp-secret]: #credentialazreposserviceprincipalsecret [credential-azrepos-sp-cert-thumbprint]: #credentialazreposserviceprincipalcertificatethumbprint diff --git a/docs/environment.md b/docs/environment.md index f321caa6c..44a50e4ee 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -991,6 +991,172 @@ export GCM_AZREPOS_MANAGEDIDENTITY="id://11111111-1111-1111-1111-111111111111" --- +### GCM_AZREPOS_WIF + +Use [Workload Identity Federation][wif] to authenticate with Azure Repos. + +The value specifies the federation scenario to use for obtaining a client +assertion to exchange for an access token. + +You must also set the following companion settings: + +- [GCM_AZREPOS_WIF_CLIENTID][gcm-azrepos-wif-clientid] +- [GCM_AZREPOS_WIF_TENANTID][gcm-azrepos-wif-tenantid] + +Depending on the scenario, additional settings may be required. + +Value|Description +-|- +`generic`|Use a user-supplied client assertion ([GCM_AZREPOS_WIF_ASSERTION][gcm-azrepos-wif-assertion]) +`managedidentity`|Use a [Managed Identity][managed-identity] to obtain the federated token ([GCM_AZREPOS_WIF_MANAGEDIDENTITY][gcm-azrepos-wif-managedidentity]) +`githubactions`|Automatically obtain an OIDC token from GitHub Actions + +For more information about workload identity federation, see the +[conceptual documentation][azrepos-wif-doc] and the Azure DevOps +[documentation][azrepos-sp-mid]. + +#### Windows + +```batch +SET GCM_AZREPOS_WIF="githubactions" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_WIF="githubactions" +``` + +**Also see: [credential.azreposWorkloadFederation][credential-azrepos-wif]** + +--- + +### GCM_AZREPOS_WIF_CLIENTID + +The client ID of the app registration / service principal to request an access +token for when using [Workload Identity Federation][wif] with +[GCM_AZREPOS_WIF][gcm-azrepos-wif]. + +#### Windows + +```batch +SET GCM_AZREPOS_WIF_CLIENTID="11111111-1111-1111-1111-111111111111" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_WIF_CLIENTID="11111111-1111-1111-1111-111111111111" +``` + +**Also see: [credential.azreposWorkloadFederationClientId][credential-azrepos-wif-clientid]** + +--- + +### GCM_AZREPOS_WIF_TENANTID + +The tenant ID of the app registration / service principal to request an access +token for when using [Workload Identity Federation][wif] with +[GCM_AZREPOS_WIF][gcm-azrepos-wif]. + +#### Windows + +```batch +SET GCM_AZREPOS_WIF_TENANTID="22222222-2222-2222-2222-222222222222" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_WIF_TENANTID="22222222-2222-2222-2222-222222222222" +``` + +**Also see: [credential.azreposWorkloadFederationTenantId][credential-azrepos-wif-tenantid]** + +--- + +### GCM_AZREPOS_WIF_AUDIENCE + +The audience to use when requesting the federated token for +[Workload Identity Federation][wif] with +[GCM_AZREPOS_WIF][gcm-azrepos-wif]. + +Defaults to `api://AzureADTokenExchange`. + +#### Windows + +```batch +SET GCM_AZREPOS_WIF_AUDIENCE="api://AzureADTokenExchange" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_WIF_AUDIENCE="api://AzureADTokenExchange" +``` + +**Also see: [credential.azreposWorkloadFederationAudience][credential-azrepos-wif-audience]** + +--- + +### GCM_AZREPOS_WIF_ASSERTION + +Specifies the client assertion token to use with the `generic` +[Workload Identity Federation][wif] scenario +([GCM_AZREPOS_WIF][gcm-azrepos-wif]). + +This setting is required when `GCM_AZREPOS_WIF` is set to `generic`. + +#### Windows + +```batch +SET GCM_AZREPOS_WIF_ASSERTION="eyJhbGci..." +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_WIF_ASSERTION="eyJhbGci..." +``` + +**Also see: [credential.azreposWorkloadFederationAssertion][credential-azrepos-wif-assertion]** + +--- + +### GCM_AZREPOS_WIF_MANAGEDIDENTITY + +Specifies the [Managed Identity][managed-identity] to use to obtain a federated +token for the `managedidentity` [Workload Identity Federation][wif] scenario +([GCM_AZREPOS_WIF][gcm-azrepos-wif]). + +This setting is required when `GCM_AZREPOS_WIF` is set to `managedidentity`. + +The value accepts the same formats as +[GCM_AZREPOS_MANAGEDIDENTITY](#gcm_azrepos_managedidentity). + +Value|Description +-|- +`system`|System-Assigned Managed Identity +`[guid]`|User-Assigned Managed Identity with the specified client ID +`id://[guid]`|User-Assigned Managed Identity with the specified client ID +`resource://[guid]`|User-Assigned Managed Identity for the associated resource + +#### Windows + +```batch +SET GCM_AZREPOS_WIF_MANAGEDIDENTITY="system" +``` + +#### macOS/Linux + +```bash +export GCM_AZREPOS_WIF_MANAGEDIDENTITY="system" +``` + +**Also see: [credential.azreposWorkloadFederationManagedIdentity][credential-azrepos-wif-managedidentity]** + +--- + ### GCM_AZREPOS_SERVICE_PRINCIPAL Specify the client and tenant IDs of a [service principal][service-principal] @@ -1186,6 +1352,12 @@ Defaults to disabled. [credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout [credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype [credential-azrepos-managedidentity]: configuration.md#credentialazreposmanagedidentity +[credential-azrepos-wif]: configuration.md#credentialazreposworkloadfederation +[credential-azrepos-wif-clientid]: configuration.md#credentialazreposworkloadfederationclientid +[credential-azrepos-wif-tenantid]: configuration.md#credentialazreposworkloadfederationtenantid +[credential-azrepos-wif-audience]: configuration.md#credentialazreposworkloadfederationaudience +[credential-azrepos-wif-assertion]: configuration.md#credentialazreposworkloadfederationassertion +[credential-azrepos-wif-managedidentity]: configuration.md#credentialazreposworkloadfederationmanagedidentity [credential-bitbucketauthmodes]: configuration.md#credentialbitbucketAuthModes [credential-cacheoptions]: configuration.md#credentialcacheoptions [credential-credentialstore]: configuration.md#credentialcredentialstore @@ -1224,6 +1396,7 @@ Defaults to disabled. [network-http-proxy]: netconfig.md#http-proxy [libsecret]: https://wiki.gnome.org/Projects/Libsecret [managed-identity]: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview +[wif]: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation [migration-guide]: migration.md#gcm_authority [passwordstore]: https://www.passwordstore.org/ [trace2-normal-docs]: https://git-scm.com/docs/api-trace2#_the_normal_format_target @@ -1235,6 +1408,13 @@ Defaults to disabled. [windows-broker]: windows-broker.md [service-principal]: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals [azrepos-sp-mid]: https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity +[azrepos-wif-doc]: azrepos-wif.md +[gcm-azrepos-wif]: #gcm_azrepos_wif +[gcm-azrepos-wif-clientid]: #gcm_azrepos_wif_clientid +[gcm-azrepos-wif-tenantid]: #gcm_azrepos_wif_tenantid +[gcm-azrepos-wif-audience]: #gcm_azrepos_wif_audience +[gcm-azrepos-wif-assertion]: #gcm_azrepos_wif_assertion +[gcm-azrepos-wif-managedidentity]: #gcm_azrepos_wif_managedidentity [gcm-azrepos-sp]: #gcm_azrepos_service_principal [gcm-azrepos-sp-secret]: #gcm_azrepos_sp_secret [gcm-azrepos-sp-cert-thumbprint]: #gcm_azrepos_sp_cert_thumbprint