Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Azure DevOps Services | Azure DevOps Server | Azure DevOps Server 2022
Aprenda a ampliar e integrar con Azure DevOps mediante las bibliotecas cliente de .NET con métodos de autenticación modernos y prácticas de codificación seguras.
Prerrequisitos
Paquetes NuGet necesarios:
- Microsoft.TeamFoundationServer.Client : API básicas de Azure DevOps
- Microsoft.VisualStudio.Services.Client : conexión y autenticación
- Microsoft.VisualStudio.Services.InteractiveClient : flujos de autenticación interactivos
Recomendaciones de autenticación:
- Aplicaciones hospedadas en Azure: uso de identidades administradas
- Canalizaciones de CI/CD: uso de entidades de servicio
- Aplicaciones interactivas: Uso de la autenticación de Microsoft Entra
- Solo escenarios heredados: uso de tokens de acceso personal
Importante
En este artículo se muestran varios métodos de autenticación para distintos escenarios. Elija el método más adecuado en función de los requisitos de seguridad y entorno de implementación.
Ejemplo de conexión principal y elemento de trabajo
En este ejemplo completo se muestran los procedimientos recomendados para conectarse a Azure DevOps y trabajar con elementos de trabajo:
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
/// <summary>
/// Demonstrates secure Azure DevOps integration with proper error handling and resource management
/// </summary>
public class AzureDevOpsService
{
private readonly VssConnection _connection;
private readonly WorkItemTrackingHttpClient _witClient;
public AzureDevOpsService(string organizationUrl, VssCredentials credentials)
{
// Create connection with proper credential management
_connection = new VssConnection(new Uri(organizationUrl), credentials);
// Get work item tracking client (reused for efficiency)
_witClient = _connection.GetClient<WorkItemTrackingHttpClient>();
}
/// <summary>
/// Creates a work item query, executes it, and returns results with proper error handling
/// </summary>
public async Task<IEnumerable<WorkItem>> GetNewBugsAsync(string projectName)
{
try
{
// Get query hierarchy with proper depth control
var queryHierarchyItems = await _witClient.GetQueriesAsync(projectName, depth: 2);
// Find 'My Queries' folder using safe navigation
var myQueriesFolder = queryHierarchyItems
.FirstOrDefault(qhi => qhi.Name.Equals("My Queries", StringComparison.OrdinalIgnoreCase));
if (myQueriesFolder == null)
{
throw new InvalidOperationException("'My Queries' folder not found in project.");
}
const string queryName = "New Bugs Query";
// Check if query already exists
var existingQuery = myQueriesFolder.Children?
.FirstOrDefault(qhi => qhi.Name.Equals(queryName, StringComparison.OrdinalIgnoreCase));
QueryHierarchyItem query;
if (existingQuery == null)
{
// Create new query with proper WIQL
query = new QueryHierarchyItem
{
Name = queryName,
Wiql = @"
SELECT [System.Id], [System.WorkItemType], [System.Title],
[System.AssignedTo], [System.State], [System.Tags]
FROM WorkItems
WHERE [System.TeamProject] = @project
AND [System.WorkItemType] = 'Bug'
AND [System.State] = 'New'
ORDER BY [System.CreatedDate] DESC",
IsFolder = false
};
query = await _witClient.CreateQueryAsync(query, projectName, myQueriesFolder.Name);
}
else
{
query = existingQuery;
}
// Execute query and get results
var queryResult = await _witClient.QueryByIdAsync(query.Id);
if (!queryResult.WorkItems.Any())
{
return Enumerable.Empty<WorkItem>();
}
// Batch process work items for efficiency
const int batchSize = 100;
var allWorkItems = new List<WorkItem>();
for (int skip = 0; skip < queryResult.WorkItems.Count(); skip += batchSize)
{
var batch = queryResult.WorkItems.Skip(skip).Take(batchSize);
var workItemIds = batch.Select(wir => wir.Id).ToArray();
// Get detailed work item information
var workItems = await _witClient.GetWorkItemsAsync(
ids: workItemIds,
fields: new[] { "System.Id", "System.Title", "System.State",
"System.AssignedTo", "System.CreatedDate" });
allWorkItems.AddRange(workItems);
}
return allWorkItems;
}
catch (Exception ex)
{
// Log error appropriately in real applications
throw new InvalidOperationException($"Failed to retrieve work items: {ex.Message}", ex);
}
}
/// <summary>
/// Properly dispose of resources
/// </summary>
public void Dispose()
{
_witClient?.Dispose();
_connection?.Dispose();
}
}
Métodos de autenticación
Autenticación de Microsoft Entra (recomendado)
Para las aplicaciones que admiten la autenticación interactiva o tienen tokens de Microsoft Entra:
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
/// <summary>
/// Authenticate using Microsoft Entra ID credentials
/// Recommended for interactive applications and modern authentication scenarios
/// </summary>
public static VssConnection CreateEntraConnection(string organizationUrl, string accessToken)
{
// Use Microsoft Entra access token for authentication
var credentials = new VssOAuthAccessTokenCredential(accessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
/// <summary>
/// For username/password scenarios (less secure, avoid when possible)
/// </summary>
public static VssConnection CreateEntraUsernameConnection(string organizationUrl, string username, string password)
{
var credentials = new VssAadCredential(username, password);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Autenticación de entidad de servicio
Para escenarios automatizados y canalizaciones de CI/CD:
using Microsoft.Identity.Client;
using Microsoft.VisualStudio.Services.Client;
/// <summary>
/// Authenticate using service principal with certificate (most secure)
/// Recommended for production automation scenarios
/// </summary>
public static async Task<VssConnection> CreateServicePrincipalConnectionAsync(
string organizationUrl,
string clientId,
string tenantId,
X509Certificate2 certificate)
{
try
{
// Create confidential client application with certificate
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithCertificate(certificate)
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
// Acquire token for Azure DevOps
var result = await app
.AcquireTokenForClient(new[] { "https://app.vssps.visualstudio.com/.default" })
.ExecuteAsync();
// Create connection with acquired token
var credentials = new VssOAuthAccessTokenCredential(result.AccessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
catch (Exception ex)
{
throw new AuthenticationException($"Failed to authenticate service principal: {ex.Message}", ex);
}
}
/// <summary>
/// Service principal with client secret (less secure than certificate)
/// </summary>
public static async Task<VssConnection> CreateServicePrincipalSecretConnectionAsync(
string organizationUrl,
string clientId,
string tenantId,
string clientSecret)
{
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
var result = await app
.AcquireTokenForClient(new[] { "https://app.vssps.visualstudio.com/.default" })
.ExecuteAsync();
var credentials = new VssOAuthAccessTokenCredential(result.AccessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Autenticación de identidad administrada
Para aplicaciones hospedadas en Azure (recomendadas para escenarios en la nube):
using Azure.Identity;
using Azure.Core;
using Microsoft.VisualStudio.Services.Client;
/// <summary>
/// Authenticate using managed identity (most secure for Azure-hosted apps)
/// No credentials to manage - Azure handles everything automatically
/// </summary>
public static async Task<VssConnection> CreateManagedIdentityConnectionAsync(string organizationUrl)
{
try
{
// Use system-assigned managed identity
var credential = new ManagedIdentityCredential();
// Acquire token for Azure DevOps
var tokenRequest = new TokenRequestContext(new[] { "https://app.vssps.visualstudio.com/.default" });
var tokenResponse = await credential.GetTokenAsync(tokenRequest);
// Create connection with managed identity token
var credentials = new VssOAuthAccessTokenCredential(tokenResponse.Token);
return new VssConnection(new Uri(organizationUrl), credentials);
}
catch (Exception ex)
{
throw new AuthenticationException($"Failed to authenticate with managed identity: {ex.Message}", ex);
}
}
/// <summary>
/// Use user-assigned managed identity with specific client ID
/// </summary>
public static async Task<VssConnection> CreateUserAssignedManagedIdentityConnectionAsync(
string organizationUrl,
string managedIdentityClientId)
{
var credential = new ManagedIdentityCredential(managedIdentityClientId);
var tokenRequest = new TokenRequestContext(new[] { "https://app.vssps.visualstudio.com/.default" });
var tokenResponse = await credential.GetTokenAsync(tokenRequest);
var credentials = new VssOAuthAccessTokenCredential(tokenResponse.Token);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Autenticación interactiva (solo .NET Framework)
Para las aplicaciones de escritorio que requieren el inicio de sesión del usuario:
/// <summary>
/// Interactive authentication with Visual Studio sign-in prompt
/// .NET Framework only - not supported in .NET Core/.NET 5+
/// </summary>
public static VssConnection CreateInteractiveConnection(string organizationUrl)
{
var credentials = new VssClientCredentials();
return new VssConnection(new Uri(organizationUrl), credentials);
}
Autenticación de token de acceso personal (heredado)
Advertencia
Los tokens de acceso personal están en desuso. Use métodos de autenticación modernos en su lugar. Consulte Guía de autenticación para ver las opciones de migración.
/// <summary>
/// Personal Access Token authentication (legacy - use modern auth instead)
/// Only use for migration scenarios or when modern auth isn't available
/// </summary>
public static VssConnection CreatePATConnection(string organizationUrl, string personalAccessToken)
{
var credentials = new VssBasicCredential(string.Empty, personalAccessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Ejemplos de uso completos
Función de Azure con identidad administrada
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
public class AzureDevOpsFunction
{
private readonly ILogger<AzureDevOpsFunction> _logger;
public AzureDevOpsFunction(ILogger<AzureDevOpsFunction> logger)
{
_logger = logger;
}
[Function("ProcessWorkItems")]
public async Task<string> ProcessWorkItems(
[TimerTrigger("0 0 8 * * MON")] TimerInfo timer)
{
try
{
var organizationUrl = Environment.GetEnvironmentVariable("AZURE_DEVOPS_ORG_URL");
var projectName = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PROJECT");
// Use managed identity for secure authentication
using var connection = await CreateManagedIdentityConnectionAsync(organizationUrl);
using var service = new AzureDevOpsService(organizationUrl, connection.Credentials);
var workItems = await service.GetNewBugsAsync(projectName);
_logger.LogInformation($"Processed {workItems.Count()} work items");
return $"Successfully processed {workItems.Count()} work items";
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process work items");
throw;
}
}
}
Aplicación de consola con entidad de servicio
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
class Program
{
static async Task Main(string[] args)
{
// Configure logging and configuration
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<Program>();
try
{
var settings = configuration.GetSection("AzureDevOps");
var organizationUrl = settings["OrganizationUrl"];
var projectName = settings["ProjectName"];
var clientId = settings["ClientId"];
var tenantId = settings["TenantId"];
var clientSecret = settings["ClientSecret"]; // Better: use Key Vault
// Authenticate with service principal
using var connection = await CreateServicePrincipalSecretConnectionAsync(
organizationUrl, clientId, tenantId, clientSecret);
using var service = new AzureDevOpsService(organizationUrl, connection.Credentials);
// Process work items
var workItems = await service.GetNewBugsAsync(projectName);
foreach (var workItem in workItems)
{
Console.WriteLine($"Bug {workItem.Id}: {workItem.Fields["System.Title"]}");
}
logger.LogInformation($"Successfully processed {workItems.Count()} work items");
}
catch (Exception ex)
{
logger.LogError(ex, "Application failed");
Environment.Exit(1);
}
}
}
procedimientos recomendados
Consideraciones de seguridad
Administración de credenciales:
- Nunca codificar las credenciales de forma rígida en el código fuente
- Uso de Azure Key Vault para almacenar secretos
- Preferir identidades administradas para aplicaciones hospedadas en Azure
- Use certificados en lugar de secretos de cliente para entidades de servicio
- Rotación de credenciales periódicamente siguiendo las directivas de seguridad
Control de acceso:
- Aplicar el principio de privilegios mínimos
- Utilice ámbitos específicos al adquirir tokens
- Supervisión y auditoría de eventos de autenticación
- Implementación de directivas de acceso condicional cuando corresponda
Optimización del rendimiento
Administración de conexiones:
- Reutilización de instancias de VssConnection entre operaciones
- Agrupación de clientes HTTP a través del objeto de conexión
- Implementación de patrones de eliminación adecuados
- Configurar los tiempos de espera adecuadamente
Operaciones por lotes
- Procesar elementos de trabajo en lotes (recomendado: 100 elementos)
- Uso del procesamiento paralelo para operaciones independientes
- Implementación de lógica de reintentos con retroceso exponencial
- Almacenar en caché los datos a los que se accede con frecuencia cuando proceda
Control de errores
public async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3)
{
var retryCount = 0;
var baseDelay = TimeSpan.FromSeconds(1);
while (retryCount < maxRetries)
{
try
{
return await operation();
}
catch (Exception ex) when (IsTransientError(ex) && retryCount < maxRetries - 1)
{
retryCount++;
var delay = TimeSpan.FromMilliseconds(baseDelay.TotalMilliseconds * Math.Pow(2, retryCount));
await Task.Delay(delay);
}
}
// Final attempt without catch
return await operation();
}
private static bool IsTransientError(Exception ex)
{
return ex is HttpRequestException ||
ex is TaskCanceledException ||
(ex is VssServiceException vssEx && vssEx.HttpStatusCode >= 500);
}
Guía de migración
Desde PAT hasta la autenticación moderna
Paso 1: Evaluar el uso actual
- Identificación de todas las aplicaciones mediante PAT
- Determinación de entornos de implementación (Azure frente a local)
- Evaluación de los requisitos de seguridad
Paso 2: Elegir el método de reemplazo
- Hospedado en Azure: Migración a identidades administradas
- Canalizaciones de CI/CD: uso de entidades de servicio
- Aplicaciones interactivas: Implementación de la autenticación de Microsoft Entra
- Aplicaciones de escritorio: Considere usar el flujo de código del dispositivo
Paso 3: Implementación
- Actualización del código de autenticación mediante los ejemplos anteriores
- Prueba exhaustiva en el entorno de desarrollo
- Implementación incremental en producción
- Supervisión de problemas de autenticación
Para obtener instrucciones detalladas sobre la migración, consulte Reemplazar PAT por tokens de Microsoft Entra.