Skip to content

Commit 2205e1f

Browse files
poojashahmsclaude
andcommitted
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 <noreply@anthropic.com>
1 parent c4be6b2 commit 2205e1f

1 file changed

Lines changed: 35 additions & 7 deletions

File tree

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class AzureReposHostProvider : DisposableObject, IHostProvider, IConfigur
2020
private readonly IMicrosoftAuthentication _msAuth;
2121
private readonly IAzureDevOpsAuthorityCache _authorityCache;
2222
private readonly IAzureReposBindingManager _bindingManager;
23+
// Set to true if PAT creation was blocked by org policy this invocation, forcing OAuth fallback.
24+
private bool _forcedOAuth;
2325

2426
public AzureReposHostProvider(ICommandContext context)
2527
: this(context, new AzureDevOpsRestApi(context), new MicrosoftAuthentication(context),
@@ -118,20 +120,28 @@ public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)
118120

119121
// No existing credential was found, create a new one
120122
_context.Trace.WriteLine("Creating new credential...");
121-
credential = await GeneratePersonalAccessTokenAsync(input);
122-
_context.Trace.WriteLine("Credential created.");
123+
try
124+
{
125+
credential = await GeneratePersonalAccessTokenAsync(input);
126+
_context.Trace.WriteLine("Credential created.");
127+
return new GetCredentialResult(credential);
128+
}
129+
catch (Exception ex) when (IsPatPolicyViolation(ex))
130+
{
131+
HandlePatPolicyViolation();
132+
// Fall through to OAuth path below
133+
}
123134
}
124135
else
125136
{
126137
_context.Trace.WriteLine("Existing credential found.");
138+
return new GetCredentialResult(credential);
127139
}
128-
129-
return new GetCredentialResult(credential);
130140
}
131-
else
141+
142+
// Include the username request here so that we may use it as an override
143+
// for user account lookups when getting Azure Access Tokens.
132144
{
133-
// Include the username request here so that we may use it as an override
134-
// for user account lookups when getting Azure Access Tokens.
135145
var azureResult = await GetAzureAccessTokenAsync(input);
136146
var azureCredential = new GitCredential(azureResult.AccountUpn, azureResult.AccessToken);
137147
return new GetCredentialResult(azureCredential);
@@ -485,8 +495,26 @@ private static string GetAccountNameForCredentialQuery(InputArguments input)
485495
/// Check if Azure DevOps Personal Access Tokens should be used or not.
486496
/// </summary>
487497
/// <returns>True if Personal Access Tokens should be used, false otherwise.</returns>
498+
private static bool IsPatPolicyViolation(Exception ex) =>
499+
ex.Message.Contains("DisablePatCreationPolicyViolation", StringComparison.OrdinalIgnoreCase);
500+
501+
private void HandlePatPolicyViolation()
502+
{
503+
_context.Trace.WriteLine("PAT creation blocked by org policy, falling back to OAuth for this request.");
504+
_context.Streams.Error.WriteLine(
505+
"warning: PAT creation is disabled by your Azure DevOps organization policy.");
506+
_context.Streams.Error.WriteLine(
507+
"hint: To permanently use OAuth, run:");
508+
_context.Streams.Error.WriteLine(
509+
$"hint: git config --global credential.{AzureDevOpsConstants.GitConfiguration.Credential.CredentialType} {AzureDevOpsConstants.OAuthCredentialType}");
510+
511+
_forcedOAuth = true;
512+
}
513+
488514
private bool UsePersonalAccessTokens()
489515
{
516+
if (_forcedOAuth) return false;
517+
490518
// Default to using PATs except on DevBox where we prefer OAuth tokens
491519
bool defaultValue = !PlatformUtils.IsDevBox();
492520

0 commit comments

Comments
 (0)