C# Azure DevOps Repo连接工具突发Unauthorized未授权报错
问题描述
- 自行开发的C#工具用于连接Azure DevOps Repo下载Zip格式文件包,解压后通过文本查找替换完成文件自定义配置,此前已稳定运行超过4个月。
- 当月8日(周五)前工具运行完全正常,11日(周一)运行时开始抛出未授权错误;3位使用同款工具的同事运行均正常,仅本地环境报错,确认本地设备未做过相关配置变更。
相关核心代码
using System; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.Client; using Microsoft.TeamFoundation.SourceControl.WebApi; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.VisualStudio.Services.WebApi; using System.IO; using System.IO.Compression; namespace ########.######.####.####### { class Program { const String c_collectionUri = "https://dev.azure.com/#################"; const String c_projectName = "########.######.#######.######.#########"; const String c_variableGroupId = "########-####-####-####-############"; // id for the LIVE project / repos const String download_dir = @"C:\###########\"; const String dev_short_name_txt = "$$dev_shortname$$"; const String dev_long_name_txt = "$$dev_long_name$$"; const String todays_date_txt = "$$todays_date$$"; static System.Collections.Specialized.StringCollection log = new System.Collections.Specialized.StringCollection(); private static string dev_short_name; private static string dev_long_name; private static string todays_date; static void Main(string[] args) { // gets just the username, e.g. ############ dev_short_name = Environment.UserName; Console.WriteLine("Who are you : " + dev_short_name); DateTime today = DateTime.Today; // As DateTime todays_date = today.ToString("dd/MM/yyyy"); // As String Console.WriteLine("Today is : " + todays_date); // Interactively ask the user for credentials, caching them so the user isn't constantly prompted VssCredentials creds = new VssClientCredentials(); creds.Storage = new VssClientCredentialStorage(); // Connect to Azure DevOps Services VssConnection connection = new VssConnection(new Uri(c_collectionUri), creds); Console.WriteLine(""); Console.WriteLine("Projects...."); Console.WriteLine(""); ProjectHttpClient projectClient = connection.GetClient<ProjectHttpClient>(); TeamHttpClient teamClient = connection.GetClient<TeamHttpClient>(); // Call to get the list of projects IEnumerable<TeamProjectReference> projects = projectClient.GetProjects().Result; Dictionary<TeamProjectReference, IEnumerable<WebApiTeam>> results = new Dictionary<TeamProjectReference, IEnumerable<WebApiTeam>>(); // Iterate over the returned projects foreach (var project in projects) { // Get the teams for the project IEnumerable<WebApiTeam> teams = teamClient.GetTeamsAsync(project.Name).Result; // Add the project/teams item to the results dictionary results.Add(project, teams); Console.WriteLine(" " + project.Id + " " + project.Name);
运行时错误输出
Who are you : ############### Today is : 14/07/2022 Projects.... Unhandled Exception: Microsoft.VisualStudio.Services.WebApi.VssServiceResponseException: Unauthorized at Microsoft.VisualStudio.Services.WebApi.VssHttpClientBase.<HandleResponseAsync>d__53.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.VssHttpClientBase.<SendAsync>d__51.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.VssHttpClientBase.<SendAsync>d__47`1.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.Location.Client.LocationHttpClient.<GetConnectionDataAsync>d__6.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.VssServerDataProvider.<GetConnectionDataAsync>d__56.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.VssServerDataProvider.<ConnectAsync>d__41.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.VssServerDataProvider.<EnsureConnectedAsync>d__39.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.VssServerDataProvider.<CheckForServerUpdatesAsync>d__38.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.VssServerDataProvider.<FindServiceDefinitionAsync>d__35.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.VssServerDataProvider.<LocationForCurrentConnectionAsync>d__29.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.LocationService.<ResolveLocationDataAsync>d__3.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.Location.LocationService.<GetLocationDataAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.VssConnection.<GetClientInstanceAsync>d__20.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.VssConnection.<GetClientServiceImplAsync>d__17.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.VssConnection.<GetClientAsync>d__14`1.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.VisualStudio.Services.WebApi.TaskExtensions.SyncResult[T](Task`1 task) at Microsoft.VisualStudio.Services.WebApi.VssConnection.GetClient[T]() at ########.######.####.#######.Program.Main(String[] args) in C:\########\########.######.####.#######\########.######.####.#######\Program.cs:line 60
已尝试的排查方案
- 删除路径
C:\Users\###########\AppData\Local\.IdentityService目录 - 清空Edge、Chrome浏览器的临时Internet文件与缓存文件夹
- 移除Windows凭据管理器中所有通用凭据
- 替换VssCredentials、VssClientCredentialStorage、VssConnection相关鉴权逻辑,尝试多种鉴权实现方式,测试代码如下:
// Interactively ask the user for credentials, caching them so the user isn't constantly prompted VssCredentials creds = new VssClientCredentials(); creds.Storage = new VssClientCredentialStorage(); Console.WriteLine("creds : " + creds.Windows.ToString()); Console.WriteLine("creds2 : " + creds.Storage.ToString()); // Connect to Azure DevOps Services VssConnection connection; ProjectHttpClient projectClient; try { connection = new VssConnection(new Uri(c_collectionUri), creds); projectClient = connection.GetClient<ProjectHttpClient>(); } catch (VssServiceResponseException e) { Console.WriteLine(e); connection = new VssConnection(new Uri(c_collectionUri), new VssAadCredential()); //connection = new VssConnection(new Uri(c_collectionUri), new VssBasicCredential(string.Empty, personalAccessToken)); //connection = new VssConnection(new Uri(c_collectionUri), new VssBasicCredential("##############","###################################")); projectClient = connection.GetClient<ProjectHttpClient>(); } //VssConnection connection = new VssConnection(new Uri(c_collectionUri), creds); //VssConnection connection = new VssConnection(new Uri(c_collectionUri), new VssAadCredential());
经过上述尝试后仍无法完成鉴权连接。
解决方案
这个报错是2022年7月Azure DevOps分批次下线旧版Azure AD身份验证协议导致的,和代码业务逻辑无关。同事能正常运行是因为其账号所属租户暂未切换到新协议,你的账号刚好在首批切换范围内,16.x及更早版本的Azure DevOps客户端SDK未适配新鉴权流程,就会固定返回401未授权,和本地缓存、凭据是否清理没有直接关系。按以下步骤操作即可修复:
- 升级项目中所有Azure DevOps相关NuGet包到最新稳定版,必须覆盖以下包:
Microsoft.TeamFoundationServer.ClientMicrosoft.VisualStudio.Services.ClientMicrosoft.VisualStudio.Services.InteractiveClient
旧版本SDK缺少新鉴权端点的适配逻辑,是问题的核心诱因。
- 兜底鉴权不要用无参构造的
VssAadCredential(),这个实现不会在新协议下正确触发交互式登录,稳定性最高的方案是使用个人访问令牌(PAT)鉴权:
在Azure DevOps个人设置页生成一个仅授予「代码(读取)」权限的PAT,替换鉴权代码为:
该方式完全绕开本地AD缓存、浏览器Cookie依赖,不受本地凭据损坏、租户鉴权协议升级影响。// 将personalAccessToken替换为你生成的令牌字符串 connection = new VssConnection(new Uri(c_collectionUri), new VssBasicCredential(string.Empty, personalAccessToken)); - 如果要保留交互式登录逻辑,升级SDK后修改凭据初始化代码,强制跳过本地损坏的缓存凭据触发重新授权:
首次运行时会弹出微软登录窗口,完成授权后新的有效凭据会自动写入本地存储,后续运行无需重复登录。VssCredentials creds = new VssClientCredentials( new WindowsCredential(), new VssFederatedCredential(true), CredentialPromptType.PromptIfNeeded); creds.Storage = new VssClientCredentialStorage(); - 补充清理遗漏的缓存路径:删除
C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Cookies下所有和microsoftonline、dev.azure.com相关的Cookie文件,之前清理的Local目录下的IdentityService是旧版SDK的缓存位置,新版SDK部分鉴权缓存存储在Roaming目录下。
调试时先单独测试获取项目列表的接口,确认鉴权通过后再运行后续下载、解压、文本替换逻辑,减少无效调试时间。
内容的提问来源于stack exchange,提问作者Mick
相关产品推荐
相关产品推荐

