Azure AD vs Entra ID: Migration Guide for .NET Apps (2025)
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.