You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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未授权,和本地缓存、凭据是否清理没有直接关系。按以下步骤操作即可修复:

  1. 升级项目中所有Azure DevOps相关NuGet包到最新稳定版,必须覆盖以下包:
    • Microsoft.TeamFoundationServer.Client
    • Microsoft.VisualStudio.Services.Client
    • Microsoft.VisualStudio.Services.InteractiveClient
      旧版本SDK缺少新鉴权端点的适配逻辑,是问题的核心诱因。
  2. 兜底鉴权不要用无参构造的VssAadCredential(),这个实现不会在新协议下正确触发交互式登录,稳定性最高的方案是使用个人访问令牌(PAT)鉴权:
    在Azure DevOps个人设置页生成一个仅授予「代码(读取)」权限的PAT,替换鉴权代码为:
    // 将personalAccessToken替换为你生成的令牌字符串
    connection = new VssConnection(new Uri(c_collectionUri), new VssBasicCredential(string.Empty, personalAccessToken));
    
    该方式完全绕开本地AD缓存、浏览器Cookie依赖,不受本地凭据损坏、租户鉴权协议升级影响。
  3. 如果要保留交互式登录逻辑,升级SDK后修改凭据初始化代码,强制跳过本地损坏的缓存凭据触发重新授权:
    VssCredentials creds = new VssClientCredentials(
        new WindowsCredential(),
        new VssFederatedCredential(true),
        CredentialPromptType.PromptIfNeeded);
    creds.Storage = new VssClientCredentialStorage();
    
    首次运行时会弹出微软登录窗口,完成授权后新的有效凭据会自动写入本地存储,后续运行无需重复登录。
  4. 补充清理遗漏的缓存路径:删除C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Cookies下所有和microsoftonline、dev.azure.com相关的Cookie文件,之前清理的Local目录下的IdentityService是旧版SDK的缓存位置,新版SDK部分鉴权缓存存储在Roaming目录下。

调试时先单独测试获取项目列表的接口,确认鉴权通过后再运行后续下载、解压、文本替换逻辑,减少无效调试时间。


内容的提问来源于stack exchange,提问作者Mick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:06:21