From 2205e1fee5c45a7e2d768288f7bbe617cce54a49 Mon Sep 17 00:00:00 2001 From: Pooja Shah Date: Sat, 16 May 2026 19:18:20 +0530 Subject: [PATCH] azrepos: fall back to OAuth when org policy blocks PAT creation When an Azure DevOps organization has disabled PAT creation via the DisablePatCreationPolicyViolation policy, GCM previously surfaced a raw fatal error with no guidance. This change catches that specific error, falls back to OAuth for the current invocation, and prints a clear warning with the exact git config command needed to make OAuth permanent. A `_forcedOAuth` instance flag ensures that StoreCredentialAsync and EraseCredentialAsync use the OAuth path consistently within the same process, working around the Settings cache which is populated once per invocation. Co-Authored-By: Claude Sonnet 4.6 --- .../AzureReposHostProvider.cs | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 9a916a236..41fef52cf 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -20,6 +20,8 @@ public class AzureReposHostProvider : DisposableObject, IHostProvider, IConfigur private readonly IMicrosoftAuthentication _msAuth; private readonly IAzureDevOpsAuthorityCache _authorityCache; private readonly IAzureReposBindingManager _bindingManager; + // Set to true if PAT creation was blocked by org policy this invocation, forcing OAuth fallback. + private bool _forcedOAuth; public AzureReposHostProvider(ICommandContext context) : this(context, new AzureDevOpsRestApi(context), new MicrosoftAuthentication(context), @@ -118,20 +120,28 @@ public async Task GetCredentialAsync(InputArguments input) // No existing credential was found, create a new one _context.Trace.WriteLine("Creating new credential..."); - credential = await GeneratePersonalAccessTokenAsync(input); - _context.Trace.WriteLine("Credential created."); + try + { + credential = await GeneratePersonalAccessTokenAsync(input); + _context.Trace.WriteLine("Credential created."); + return new GetCredentialResult(credential); + } + catch (Exception ex) when (IsPatPolicyViolation(ex)) + { + HandlePatPolicyViolation(); + // Fall through to OAuth path below + } } else { _context.Trace.WriteLine("Existing credential found."); + return new GetCredentialResult(credential); } - - return new GetCredentialResult(credential); } - else + + // Include the username request here so that we may use it as an override + // for user account lookups when getting Azure Access Tokens. { - // Include the username request here so that we may use it as an override - // for user account lookups when getting Azure Access Tokens. var azureResult = await GetAzureAccessTokenAsync(input); var azureCredential = new GitCredential(azureResult.AccountUpn, azureResult.AccessToken); return new GetCredentialResult(azureCredential); @@ -485,8 +495,26 @@ private static string GetAccountNameForCredentialQuery(InputArguments input) /// Check if Azure DevOps Personal Access Tokens should be used or not. /// /// True if Personal Access Tokens should be used, false otherwise. + private static bool IsPatPolicyViolation(Exception ex) => + ex.Message.Contains("DisablePatCreationPolicyViolation", StringComparison.OrdinalIgnoreCase); + + private void HandlePatPolicyViolation() + { + _context.Trace.WriteLine("PAT creation blocked by org policy, falling back to OAuth for this request."); + _context.Streams.Error.WriteLine( + "warning: PAT creation is disabled by your Azure DevOps organization policy."); + _context.Streams.Error.WriteLine( + "hint: To permanently use OAuth, run:"); + _context.Streams.Error.WriteLine( + $"hint: git config --global credential.{AzureDevOpsConstants.GitConfiguration.Credential.CredentialType} {AzureDevOpsConstants.OAuthCredentialType}"); + + _forcedOAuth = true; + } + private bool UsePersonalAccessTokens() { + if (_forcedOAuth) return false; + // Default to using PATs except on DevBox where we prefer OAuth tokens bool defaultValue = !PlatformUtils.IsDevBox();