Azure AD vs Entra ID: Migration Guide for .NET Apps (2025)

Jan 19, 2025
azureentra-iddotnetauthentication
0

Microsoft's transition from Azure Active Directory (Azure AD) to Microsoft Entra ID represents a significant evolution in identity and access management. For .NET developers, this change brings new opportunities and challenges. In this comprehensive guide, we'll explore the differences between Azure AD and Entra ID, provide step-by-step migration instructions, and share best practices for .NET applications.

Understanding the Transition

What is Microsoft Entra ID?

Microsoft Entra ID is the new name for Azure Active Directory, but it's more than just a rebrand. It represents Microsoft's vision for a unified identity platform that goes beyond traditional directory services to include:

  • Identity and access management - Core directory services
  • Identity protection - Advanced security features
  • Privileged identity management - Just-in-time access
  • Identity governance - Lifecycle management
  • External identities - B2B and B2C capabilities

Key Differences: Azure AD vs Entra ID

Aspect Azure AD Entra ID
Naming Azure Active Directory Microsoft Entra ID
Scope Directory services Unified identity platform
APIs Azure AD Graph API Microsoft Graph API
Authentication Azure AD Authentication Microsoft Identity Platform
SDKs Azure AD SDK Microsoft Authentication Library (MSAL)
Token Format Azure AD tokens Microsoft Identity Platform tokens

Migration Prerequisites

1. Environment Assessment

Before starting the migration, assess your current environment:

// Check current Azure AD configuration
public class AzureAdConfiguration
{
    public string TenantId { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
    public string Instance { get; set; } = "https://login.microsoftonline.com/";
    public string GraphEndpoint { get; set; } = "https://graph.windows.net/";
    public string[] Scopes { get; set; }
}

// Validate current setup
public async Task<bool> ValidateCurrentConfiguration(AzureAdConfiguration config)
{
    try
    {
        var client = new HttpClient();
        var token = await GetAccessTokenAsync(config);
        
        // Test Graph API access
        var response = await client.GetAsync($"{config.GraphEndpoint}{config.TenantId}/users");
        return response.IsSuccessStatusCode;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Configuration validation failed: {ex.Message}");
        return false;
    }
}

2. Dependencies Update

Update your NuGet packages to the latest versions:

<!-- Remove old Azure AD packages -->
<!-- <PackageReference Include="Microsoft.Azure.ActiveDirectory.GraphClient" Version="2.1.2" /> -->
<!-- <PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.9" /> -->

<!-- Add new Microsoft Identity packages -->
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.1" />
<PackageReference Include="Microsoft.Graph" Version="5.25.0" />
<PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.7" />

Step-by-Step Migration Process

Step 1: Update Authentication Configuration

Old Azure AD Configuration

// appsettings.json (Old)
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "yourdomain.onmicrosoft.com",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "ClientSecret": "your-client-secret",
    "GraphEndpoint": "https://graph.windows.net/"
  }
}

New Entra ID Configuration

// appsettings.json (New)
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "yourdomain.onmicrosoft.com",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "ClientSecret": "your-client-secret",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath": "/signout-oidc"
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": "User.Read"
  }
}

Step 2: Update Startup Configuration

Old Azure AD Startup

// Startup.cs (Old)
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
        .AddAzureAD(options => Configuration.Bind("AzureAd", options));
    
    services.AddMvc();
}

New Entra ID Startup

// Program.cs (New)
public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    
    // Add Microsoft Identity
    builder.Services.AddMicrosoftIdentityWebAppAuthentication(
        builder.Configuration, "AzureAd");
    
    // Add Microsoft Graph
    builder.Services.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"));
    
    // Add controllers
    builder.Services.AddControllersWithViews();
    
    var app = builder.Build();
    
    // Configure pipeline
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();
    
    app.Run();
}

Step 3: Update Authentication Code

Old Azure AD Authentication

// Old Azure AD authentication
public class AzureAdService
{
    private readonly string _tenantId;
    private readonly string _clientId;
    private readonly string _clientSecret;
    
    public async Task<string> GetAccessTokenAsync()
    {
        var authContext = new AuthenticationContext($"https://login.microsoftonline.com/{_tenantId}");
        var clientCredential = new ClientCredential(_clientId, _clientSecret);
        
        var result = await authContext.AcquireTokenAsync(
            "https://graph.windows.net/",
            clientCredential);
            
        return result.AccessToken;
    }
}

New Entra ID Authentication

// New Entra ID authentication
public class EntraIdService
{
    private readonly IConfiguration _configuration;
    private readonly IConfidentialClientApplication _app;
    
    public EntraIdService(IConfiguration configuration)
    {
        _configuration = configuration;
        _app = ConfidentialClientApplicationBuilder
            .Create(_configuration["AzureAd:ClientId"])
            .WithClientSecret(_configuration["AzureAd:ClientSecret"])
            .WithAuthority($"https://login.microsoftonline.com/{_configuration["AzureAd:TenantId"]}")
            .Build();
    }
    
    public async Task<string> GetAccessTokenAsync()
    {
        var scopes = new[] { "https://graph.microsoft.com/.default" };
        var result = await _app.AcquireTokenForClient(scopes).ExecuteAsync();
        return result.AccessToken;
    }
}

Step 4: Update Graph API Calls

Old Azure AD Graph API

// Old Azure AD Graph API
public class AzureAdGraphService
{
    private readonly string _accessToken;
    private readonly string _graphEndpoint;
    
    public async Task<List<User>> GetUsersAsync()
    {
        var client = new HttpClient();
        client.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", _accessToken);
        
        var response = await client.GetAsync($"{_graphEndpoint}users");
        var content = await response.Content.ReadAsStringAsync();
        
        var users = JsonSerializer.Deserialize<GraphResponse<User>>(content);
        return users.Value;
    }
}

New Microsoft Graph API

// New Microsoft Graph API
public class MicrosoftGraphService
{
    private readonly GraphServiceClient _graphServiceClient;
    
    public MicrosoftGraphService(GraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }
    
    public async Task<List<User>> GetUsersAsync()
    {
        var users = await _graphServiceClient.Users
            .GetAsync();
            
        return users.Value.ToList();
    }
    
    public async Task<User> GetUserAsync(string userId)
    {
        return await _graphServiceClient.Users[userId]
            .GetAsync();
    }
}

Advanced Migration Scenarios

1. Multi-Tenant Applications

// Multi-tenant configuration
public void ConfigureServices(IServiceCollection services)
{
    services.AddMicrosoftIdentityWebAppAuthentication(
        builder.Configuration, "AzureAd")
        .EnableTokenAcquisitionToCallDownstreamApi()
        .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
        .AddInMemoryTokenCaches();
    
    // Configure for multi-tenant
    services.Configure<OpenIdConnectOptions>(
        OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.TokenValidationParameters.ValidateIssuer = false;
            options.TokenValidationParameters.IssuerValidator = (issuer, token, parameters) => issuer;
        });
}

2. B2C Applications

// B2C configuration
public void ConfigureServices(IServiceCollection services)
{
    services.AddMicrosoftIdentityWebAppAuthentication(
        builder.Configuration, "AzureAdB2C")
        .EnableTokenAcquisitionToCallDownstreamApi()
        .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
        .AddInMemoryTokenCaches();
}

// appsettings.json for B2C
{
  "AzureAdB2C": {
    "Instance": "https://yourtenant.b2clogin.com/",
    "Domain": "yourtenant.onmicrosoft.com",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "ClientSecret": "your-client-secret",
    "SignUpSignInPolicyId": "B2C_1_signupsignin",
    "CallbackPath": "/signin-oidc"
  }
}

3. Custom Token Validation

// Custom token validation
public class CustomTokenValidationService
{
    private readonly IConfiguration _configuration;
    
    public async Task<bool> ValidateTokenAsync(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = $"https://login.microsoftonline.com/{_configuration["AzureAd:TenantId"]}/v2.0",
            ValidAudience = _configuration["AzureAd:ClientId"],
            IssuerSigningKey = await GetSigningKeyAsync()
        };
        
        try
        {
            tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
            return true;
        }
        catch
        {
            return false;
        }
    }
    
    private async Task<SecurityKey> GetSigningKeyAsync()
    {
        // Implementation to get signing key from Microsoft
        // This is typically handled by the framework
        throw new NotImplementedException();
    }
}

Common Migration Challenges

1. Token Format Changes

Problem: Tokens from Entra ID have different claims and structure.

Solution:

// Handle new token format
public class TokenClaimsService
{
    public UserInfo ExtractUserInfo(ClaimsPrincipal principal)
    {
        return new UserInfo
        {
            ObjectId = principal.FindFirst("oid")?.Value,
            TenantId = principal.FindFirst("tid")?.Value,
            Name = principal.FindFirst("name")?.Value,
            Email = principal.FindFirst("preferred_username")?.Value,
            GivenName = principal.FindFirst("given_name")?.Value,
            FamilyName = principal.FindFirst("family_name")?.Value
        };
    }
}

2. API Permission Changes

Problem: Some Azure AD Graph API permissions don't have direct equivalents in Microsoft Graph.

Solution:

// Map old permissions to new scopes
public class PermissionMappingService
{
    private readonly Dictionary<string, string> _permissionMap = new()
    {
        { "User.Read.All", "User.Read.All" },
        { "Directory.Read.All", "Directory.Read.All" },
        { "Group.Read.All", "Group.Read.All" },
        { "Application.Read.All", "Application.Read.All" }
    };
    
    public string MapPermission(string oldPermission)
    {
        return _permissionMap.TryGetValue(oldPermission, out var newPermission) 
            ? newPermission 
            : oldPermission;
    }
}

3. Error Handling Updates

Problem: Error responses from Microsoft Graph have different formats.

Solution:

// Updated error handling
public class GraphErrorHandler
{
    public async Task<T> HandleGraphCallAsync<T>(Func<Task<T>> graphCall)
    {
        try
        {
            return await graphCall();
        }
        catch (ServiceException ex)
        {
            switch (ex.Error.Code)
            {
                case "Forbidden":
                    throw new UnauthorizedAccessException("Insufficient permissions", ex);
                case "NotFound":
                    throw new ArgumentException("Resource not found", ex);
                case "TooManyRequests":
                    // Implement retry logic
                    await Task.Delay(1000);
                    return await graphCall();
                default:
                    throw new Exception($"Graph API error: {ex.Error.Message}", ex);
            }
        }
    }
}

Testing Migration

1. Unit Tests

[Test]
public async Task GetAccessTokenAsync_ShouldReturnValidToken()
{
    // Arrange
    var configuration = new ConfigurationBuilder()
        .AddInMemoryCollection(new Dictionary<string, string>
        {
            {"AzureAd:ClientId", "test-client-id"},
            {"AzureAd:ClientSecret", "test-client-secret"},
            {"AzureAd:TenantId", "test-tenant-id"}
        })
        .Build();
    
    var service = new EntraIdService(configuration);
    
    // Act
    var token = await service.GetAccessTokenAsync();
    
    // Assert
    Assert.IsNotNull(token);
    Assert.IsTrue(token.Length > 0);
}

2. Integration Tests

[Test]
public async Task GetUsersAsync_ShouldReturnUsers()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddMicrosoftIdentityWebAppAuthentication(
        Configuration, "AzureAd");
    services.AddMicrosoftGraph(Configuration.GetSection("MicrosoftGraph"));
    
    var provider = services.BuildServiceProvider();
    var graphService = provider.GetRequiredService<GraphServiceClient>();
    
    // Act
    var users = await graphService.Users.GetAsync();
    
    // Assert
    Assert.IsNotNull(users);
    Assert.IsTrue(users.Value.Count > 0);
}

Performance Optimization

1. Token Caching

// Implement token caching
public class CachedTokenService
{
    private readonly IMemoryCache _cache;
    private readonly EntraIdService _entraIdService;
    
    public async Task<string> GetCachedTokenAsync()
    {
        const string cacheKey = "access_token";
        
        if (_cache.TryGetValue(cacheKey, out string cachedToken))
        {
            return cachedToken;
        }
        
        var token = await _entraIdService.GetAccessTokenAsync();
        var cacheOptions = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(50) // Token expires in 1 hour
        };
        
        _cache.Set(cacheKey, token, cacheOptions);
        return token;
    }
}

2. Batch Operations

// Batch operations for better performance
public class BatchGraphService
{
    private readonly GraphServiceClient _graphServiceClient;
    
    public async Task<List<User>> GetUsersBatchAsync(List<string> userIds)
    {
        var batchRequest = new BatchRequestContent();
        
        for (int i = 0; i < userIds.Count; i++)
        {
            var request = _graphServiceClient.Users[userIds[i]].ToGetRequestInformation();
            batchRequest.AddBatchRequestStep(new BatchRequestStep(i.ToString(), request));
        }
        
        var response = await _graphServiceClient.Batch.PostAsync(batchRequest);
        var users = new List<User>();
        
        foreach (var step in response.Response)
        {
            if (step.Value.IsSuccessStatusCode)
            {
                var user = await step.Value.Content.ReadFromJsonAsync<User>();
                users.Add(user);
            }
        }
        
        return users;
    }
}

Security Considerations

1. Secure Configuration

// Secure configuration management
public class SecureConfigurationService
{
    public void ConfigureSecureServices(IServiceCollection services, IConfiguration configuration)
    {
        // Use Azure Key Vault for secrets
        services.AddAzureKeyVault(configuration);
        
        // Configure secure token validation
        services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ClockSkew = TimeSpan.Zero
            };
        });
    }
}

2. Conditional Access

// Handle conditional access policies
public class ConditionalAccessService
{
    public async Task<bool> ValidateConditionalAccessAsync(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadJwtToken(token);
        
        // Check for conditional access claims
        var conditionalAccessClaim = jwtToken.Claims
            .FirstOrDefault(c => c.Type == "acr");
            
        if (conditionalAccessClaim != null)
        {
            // Implement conditional access validation logic
            return await ValidateAccessPolicyAsync(conditionalAccessClaim.Value);
        }
        
        return true;
    }
}

Monitoring and Logging

1. Application Insights Integration

// Add telemetry for monitoring
public class GraphTelemetryService
{
    private readonly TelemetryClient _telemetryClient;
    
    public async Task<T> TrackGraphCallAsync<T>(string operation, Func<Task<T>> graphCall)
    {
        using var operation = _telemetryClient.StartOperation<DependencyTelemetry>(operation);
        
        try
        {
            var result = await graphCall();
            operation.Telemetry.Success = true;
            return result;
        }
        catch (Exception ex)
        {
            operation.Telemetry.Success = false;
            _telemetryClient.TrackException(ex);
            throw;
        }
    }
}

2. Custom Logging

// Custom logging for Graph API calls
public class GraphLoggingService
{
    private readonly ILogger<GraphLoggingService> _logger;
    
    public async Task<T> LogGraphCallAsync<T>(string operation, Func<Task<T>> graphCall)
    {
        _logger.LogInformation("Starting Graph API call: {Operation}", operation);
        
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            var result = await graphCall();
            stopwatch.Stop();
            
            _logger.LogInformation("Graph API call completed: {Operation} in {Duration}ms", 
                operation, stopwatch.ElapsedMilliseconds);
                
            return result;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            
            _logger.LogError(ex, "Graph API call failed: {Operation} in {Duration}ms", 
                operation, stopwatch.ElapsedMilliseconds);
                
            throw;
        }
    }
}

Migration Checklist

Pre-Migration

  • Audit current Azure AD usage
  • Update NuGet packages
  • Review API permissions and scopes
  • Test in development environment
  • Create rollback plan

During Migration

  • Update configuration files
  • Modify authentication code
  • Update Graph API calls
  • Implement error handling
  • Add logging and monitoring

Post-Migration

  • Test all functionality
  • Monitor performance and errors
  • Update documentation
  • Train team on new implementation
  • Plan for ongoing maintenance

Best Practices

1. Gradual Migration

  • Migrate one component at a time
  • Use feature flags to control rollout
  • Maintain backward compatibility during transition
  • Monitor each step carefully

2. Error Handling

  • Implement comprehensive error handling
  • Use retry policies for transient failures
  • Log all errors for debugging
  • Provide meaningful error messages to users

3. Performance

  • Use token caching to reduce API calls
  • Implement batch operations where possible
  • Monitor performance metrics
  • Optimize based on usage patterns

4. Security

  • Follow principle of least privilege
  • Use secure configuration management
  • Implement proper token validation
  • Monitor for security issues

Conclusion

Migrating from Azure AD to Entra ID is a significant undertaking that requires careful planning and execution. By following this comprehensive guide, you can ensure a smooth transition while taking advantage of the new features and capabilities that Entra ID offers.

Key takeaways:

  • Plan your migration carefully with proper testing
  • Update to Microsoft Graph API for better functionality
  • Implement proper error handling and monitoring
  • Follow security best practices throughout the process
  • Consider performance implications and optimize accordingly

Ready to start your migration? Our team at Elysiate can help you plan and execute a successful Azure AD to Entra ID migration. Contact us to learn more about our migration services.


Need help with other Azure or .NET development challenges? Explore our services to see how we can help your organization succeed.

Related posts