Azure Authentication & Identity for .NET Developers
Introduction
Azure authentication and identity management is fundamental to building secure cloud applications. This guide covers Microsoft Entra ID (formerly Azure Active Directory), managed identities, RBAC, and the Microsoft Authentication Library (MSAL) for .NET developers.
Table of Contents
- Microsoft Entra ID Fundamentals
- Authentication Flows
- Managed Identities
- Service Principals and App Registrations
- Role-Based Access Control (RBAC)
- MSAL for .NET
- Conditional Access
- B2C vs B2B Scenarios
- Token Validation and Refresh
- Best Practices
- Interview Questions
Microsoft Entra ID Fundamentals
Microsoft Entra ID (formerly Azure Active Directory/Azure AD) is Microsoftβs cloud-based identity and access management service. Itβs the backbone of authentication for Azure services and Microsoft 365.
Core Concepts
| Concept | Description |
|---|---|
| Tenant | A dedicated instance of Entra ID for an organization |
| User | An identity in the directory that can authenticate |
| Group | Collection of users for easier permission management |
| Application | A registered app that can authenticate users |
| Service Principal | Identity used by applications and services |
| Managed Identity | Automatically managed identity for Azure resources |
Directory Structure
Entra ID Tenant (contoso.onmicrosoft.com)
βββ Users
β βββ john@contoso.com
β βββ jane@contoso.com
βββ Groups
β βββ Developers
β βββ Administrators
βββ App Registrations
β βββ Web API (backend)
β βββ SPA Client (frontend)
βββ Enterprise Applications
β βββ Service Principals
βββ Roles
βββ Global Administrator
βββ Application Administrator
βββ Custom Roles
Identity Types
// Different identity types in Azure
public enum AzureIdentityType
{
// Human users with email/password
User,
// Collection of users
Group,
// Application identity (created via App Registration)
ServicePrincipal,
// Azure-managed identity for resources
ManagedIdentity,
// External identities (B2B guests)
GuestUser
}
Authentication Flows
OAuth 2.0 and OpenID Connect are the protocols used for authentication. Different scenarios require different flows.
Authorization Code Flow (Web Apps)
Best for server-side web applications where the code can be kept confidential.
// ASP.NET Core with Microsoft.Identity.Web
// Program.cs
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
// appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc"
}
}
Authorization Code Flow with PKCE (SPAs, Mobile)
Required for public clients that cannot securely store secrets.
// MSAL.js for SPA (referenced from .NET backend)
// React/Angular example configuration
const msalConfig = {
auth: {
clientId: "your-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
redirectUri: "http://localhost:3000"
}
};
// Acquiring token with PKCE (handled automatically by MSAL)
const loginRequest = {
scopes: ["api://your-api/access_as_user"]
};
Client Credentials Flow (Daemon/Service Apps)
For server-to-server communication without user interaction.
// Background service authenticating to another API
public class ApiClientService
{
private readonly IConfidentialClientApplication _msalClient;
private readonly string[] _scopes = { "api://target-api/.default" };
public ApiClientService(IConfiguration config)
{
_msalClient = ConfidentialClientApplicationBuilder
.Create(config["AzureAd:ClientId"])
.WithClientSecret(config["AzureAd:ClientSecret"])
.WithAuthority(new Uri($"https://login.microsoftonline.com/{config["AzureAd:TenantId"]}"))
.Build();
}
public async Task<string> GetAccessTokenAsync()
{
var result = await _msalClient
.AcquireTokenForClient(_scopes)
.ExecuteAsync();
return result.AccessToken;
}
}
On-Behalf-Of Flow (APIs Calling APIs)
When an API needs to call another API on behalf of the user.
// Web API calling downstream API on behalf of user
public class DownstreamApiService
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly HttpClient _httpClient;
public DownstreamApiService(
ITokenAcquisition tokenAcquisition,
HttpClient httpClient)
{
_tokenAcquisition = tokenAcquisition;
_httpClient = httpClient;
}
public async Task<string> CallDownstreamApiAsync()
{
// Acquire token for downstream API using OBO flow
var token = await _tokenAcquisition
.GetAccessTokenForUserAsync(new[] { "api://downstream-api/.default" });
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return await _httpClient.GetStringAsync("/api/data");
}
}
// Registration in DI
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
Device Code Flow (CLI Tools, IoT)
For devices without a browser or with limited input capability.
// Console application or CLI tool
public class DeviceCodeAuthenticator
{
private readonly IPublicClientApplication _msalClient;
public DeviceCodeAuthenticator(string clientId, string tenantId)
{
_msalClient = PublicClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithDefaultRedirectUri()
.Build();
}
public async Task<AuthenticationResult> AuthenticateAsync(string[] scopes)
{
return await _msalClient
.AcquireTokenWithDeviceCode(scopes, deviceCodeResult =>
{
// Display the device code to the user
Console.WriteLine(deviceCodeResult.Message);
// Example: "To sign in, use a web browser to open
// https://microsoft.com/devicelogin and enter code ABC123"
return Task.CompletedTask;
})
.ExecuteAsync();
}
}
Flow Selection Guide
Which authentication flow should I use?
βββ Web application (server-side)?
β βββ Authorization Code Flow
βββ Single-page application (SPA)?
β βββ Authorization Code Flow with PKCE
βββ Mobile/Desktop application?
β βββ Authorization Code Flow with PKCE
βββ Background service/daemon?
β βββ Client Credentials Flow
βββ API calling another API?
β βββ On-Behalf-Of Flow
βββ CLI tool or IoT device?
β βββ Device Code Flow
βββ Service running on Azure resource?
βββ Managed Identity (no flow needed!)
Managed Identities
Managed identities eliminate the need to manage credentials. Azure automatically handles the identity lifecycle.
System-Assigned vs User-Assigned
| Feature | System-Assigned | User-Assigned |
|---|---|---|
| Lifecycle | Tied to resource | Independent |
| Sharing | One resource only | Multiple resources |
| Creation | Automatic with resource | Manual creation |
| Deletion | Deleted with resource | Manual deletion |
| Use Case | Single resource needs | Shared across resources |
| Role Assignments | Per resource | Shared assignments |
System-Assigned Identity
// Enable system-assigned identity on App Service (Azure CLI)
// az webapp identity assign --name myapp --resource-group mygroup
// Using the identity in code
public class BlobStorageService
{
private readonly BlobContainerClient _containerClient;
public BlobStorageService(string storageAccountName, string containerName)
{
// DefaultAzureCredential automatically uses managed identity in Azure
var credential = new DefaultAzureCredential();
var blobServiceClient = new BlobServiceClient(
new Uri($"https://{storageAccountName}.blob.core.windows.net"),
credential);
_containerClient = blobServiceClient.GetBlobContainerClient(containerName);
}
public async Task UploadAsync(string blobName, Stream content)
{
await _containerClient.UploadBlobAsync(blobName, content);
}
}
User-Assigned Identity
// Create user-assigned identity (Azure CLI)
// az identity create --name myidentity --resource-group mygroup
// Assign to multiple resources
// az webapp identity assign --name app1 --resource-group mygroup \
// --identities /subscriptions/{sub}/resourcegroups/{rg}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myidentity
// Using specific user-assigned identity
public class KeyVaultService
{
private readonly SecretClient _secretClient;
public KeyVaultService(string keyVaultName, string userAssignedClientId)
{
// Specify the client ID of the user-assigned identity
var credential = new DefaultAzureCredential(
new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = userAssignedClientId
});
_secretClient = new SecretClient(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
credential);
}
public async Task<string> GetSecretAsync(string secretName)
{
var secret = await _secretClient.GetSecretAsync(secretName);
return secret.Value.Value;
}
}
DefaultAzureCredential Chain
DefaultAzureCredential tries multiple authentication methods in order:
// The credential chain (in order of precedence)
// 1. Environment Variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
// 2. Workload Identity (Kubernetes)
// 3. Managed Identity (Azure VMs, App Service, Functions)
// 4. Visual Studio
// 5. Azure CLI
// 6. Azure PowerShell
// 7. Azure Developer CLI
// 8. Interactive Browser (if enabled)
var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
// Customize the chain
ExcludeEnvironmentCredential = false,
ExcludeManagedIdentityCredential = false,
ExcludeVisualStudioCredential = false,
ExcludeAzureCliCredential = false,
// For development, exclude to speed up auth
ExcludeSharedTokenCacheCredential = true,
ExcludeInteractiveBrowserCredential = true
});
Best Practices for Managed Identities
// β
DO: Use managed identities instead of storing credentials
services.AddSingleton(_ =>
{
return new BlobServiceClient(
new Uri(configuration["Storage:BlobEndpoint"]),
new DefaultAzureCredential());
});
// β DON'T: Store connection strings with keys
// var connectionString = "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...";
// β
DO: Use user-assigned identity for shared access across resources
// β
DO: Apply least-privilege RBAC roles
// β
DO: Use separate identities for production and non-production
Service Principals and App Registrations
When you canβt use managed identities (e.g., on-premises apps, third-party services), use app registrations.
Creating an App Registration
// App Registration creates:
// 1. Application object (template in your tenant)
// 2. Service Principal (instance for your tenant)
// Azure CLI commands
// az ad app create --display-name "My API"
// az ad sp create --id <app-id>
// Grant API permissions
// az ad app permission add --id <app-id> \
// --api 00000003-0000-0000-c000-000000000000 \ # Microsoft Graph
// --api-permissions e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope # User.Read
Client Credentials (Certificates vs Secrets)
// Option 1: Client Secret (less secure, easier to manage)
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
// Option 2: Certificate (more secure, recommended for production)
var certificate = new X509Certificate2("certificate.pfx", "password");
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithCertificate(certificate)
.WithAuthority(authority)
.Build();
// Option 3: Certificate from Key Vault (best practice)
public async Task<X509Certificate2> GetCertificateFromKeyVaultAsync(
string keyVaultName,
string certificateName)
{
var credential = new DefaultAzureCredential();
var secretClient = new SecretClient(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
credential);
var secret = await secretClient.GetSecretAsync(certificateName);
return new X509Certificate2(
Convert.FromBase64String(secret.Value.Value),
(string)null,
X509KeyStorageFlags.MachineKeySet);
}
Multi-Tenant Applications
// Multi-tenant app registration
// Set "Supported account types" to "Accounts in any organizational directory"
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
builder.Configuration.Bind("AzureAd", options);
// Allow tokens from any tenant
options.TokenValidationParameters.ValidateIssuer = false;
// Or validate against a list of allowed tenants
options.TokenValidationParameters.IssuerValidator = (issuer, token, parameters) =>
{
var allowedTenants = new[] { "tenant1-id", "tenant2-id" };
var tenantId = issuer.Split('/')[3];
if (!allowedTenants.Contains(tenantId))
throw new SecurityTokenInvalidIssuerException("Tenant not allowed");
return issuer;
};
}, options => builder.Configuration.Bind("AzureAd", options));
Role-Based Access Control (RBAC)
RBAC provides fine-grained access management for Azure resources.
RBAC Components
Role Assignment = Security Principal + Role Definition + Scope
Security Principal (WHO) Role Definition (WHAT) Scope (WHERE)
βββ User βββ Owner βββ Management Group
βββ Group βββ Contributor βββ Subscription
βββ Service Principal βββ Reader βββ Resource Group
βββ Managed Identity βββ Storage Blob Reader βββ Resource
βββ Custom Role
Built-in Roles
| Role | Access Level | Common Use |
|---|---|---|
| Owner | Full access + can assign roles | Subscription admins |
| Contributor | Full access, no role assignment | Developers |
| Reader | View only | Auditors, support |
| User Access Administrator | Manage user access | Security admins |
Assigning Roles via Code
// Using Azure.ResourceManager.Authorization
public class RbacService
{
private readonly ArmClient _armClient;
public RbacService()
{
_armClient = new ArmClient(new DefaultAzureCredential());
}
public async Task AssignRoleAsync(
string scope,
string principalId,
string roleDefinitionId)
{
var roleAssignmentId = Guid.NewGuid().ToString();
var roleAssignment = new RoleAssignmentCreateOrUpdateContent(
new ResourceIdentifier(roleDefinitionId),
Guid.Parse(principalId));
var resource = _armClient.GetGenericResource(new ResourceIdentifier(scope));
var roleAssignments = resource.GetRoleAssignments();
await roleAssignments.CreateOrUpdateAsync(
WaitUntil.Completed,
roleAssignmentId,
roleAssignment);
}
}
// Common role definition IDs
public static class BuiltInRoles
{
public const string Owner =
"/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635";
public const string Contributor =
"/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c";
public const string Reader =
"/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7";
public const string StorageBlobDataReader =
"/providers/Microsoft.Authorization/roleDefinitions/2a2b9908-6ea1-4ae2-8e65-a410df84e7d1";
public const string StorageBlobDataContributor =
"/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe";
}
App Roles (Application-Level RBAC)
// Define app roles in App Registration manifest
{
"appRoles": [
{
"id": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"allowedMemberTypes": ["User"],
"displayName": "Administrator",
"description": "Can manage all aspects",
"value": "Admin"
},
{
"id": "b24988ac-6180-42a0-ab88-20f7382dd24c",
"allowedMemberTypes": ["User"],
"displayName": "Reader",
"description": "Can view data",
"value": "Reader"
}
]
}
// Check app roles in API
[Authorize(Roles = "Admin")]
[HttpDelete("users/{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
// Only users with Admin app role can access
}
// Or check programmatically
public bool IsAdmin(ClaimsPrincipal user)
{
return user.Claims
.Where(c => c.Type == ClaimTypes.Role || c.Type == "roles")
.Any(c => c.Value == "Admin");
}
MSAL for .NET
Microsoft Authentication Library (MSAL) is the recommended library for authentication.
Installation and Setup
<!-- Install NuGet packages -->
<PackageReference Include="Microsoft.Identity.Client" Version="4.60.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="2.17.0" />
Token Acquisition Best Practices
public class TokenService
{
private readonly IConfidentialClientApplication _msalClient;
private readonly string[] _scopes = { "https://graph.microsoft.com/.default" };
public TokenService(IConfiguration config)
{
_msalClient = ConfidentialClientApplicationBuilder
.Create(config["AzureAd:ClientId"])
.WithClientSecret(config["AzureAd:ClientSecret"])
.WithAuthority(new Uri($"https://login.microsoftonline.com/{config["AzureAd:TenantId"]}"))
.Build();
// Enable token caching
_msalClient.AddInMemoryTokenCache();
}
public async Task<string> GetTokenAsync()
{
AuthenticationResult result;
try
{
// β
ALWAYS try silent acquisition first (from cache)
var accounts = await _msalClient.GetAccountsAsync();
result = await _msalClient
.AcquireTokenSilent(_scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Cache miss - acquire new token
result = await _msalClient
.AcquireTokenForClient(_scopes)
.ExecuteAsync();
}
return result.AccessToken;
}
}
Token Cache Serialization
// For web apps - use distributed cache
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
// With Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
});
// Or SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("TokenCache");
options.SchemaName = "dbo";
options.TableName = "TokenCache";
});
Error Handling
public async Task<AuthenticationResult> AcquireTokenWithRetryAsync()
{
try
{
return await _msalClient
.AcquireTokenForClient(_scopes)
.ExecuteAsync();
}
catch (MsalServiceException ex) when (ex.ErrorCode == "temporarily_unavailable")
{
// Retry with exponential backoff
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, _retryCount)));
return await AcquireTokenWithRetryAsync();
}
catch (MsalServiceException ex) when (ex.ErrorCode == "invalid_grant")
{
// Token was revoked - clear cache and re-authenticate
var accounts = await _msalClient.GetAccountsAsync();
foreach (var account in accounts)
{
await _msalClient.RemoveAsync(account);
}
throw new AuthenticationRequiredException("Re-authentication required");
}
catch (MsalClientException ex) when (ex.ErrorCode == "multiple_matching_tokens")
{
// Multiple tokens in cache - clear and retry
// This shouldn't happen with proper cache management
throw;
}
}
Conditional Access
Conditional Access policies enforce additional requirements based on conditions.
Common Policies
| Policy | Condition | Action |
|---|---|---|
| Require MFA | All cloud apps | Require multi-factor authentication |
| Block legacy auth | Legacy protocols | Block access |
| Require compliant device | Sensitive apps | Require device compliance |
| Location-based | Outside corporate network | Require MFA or block |
Handling CAE (Continuous Access Evaluation)
// CAE enables real-time policy enforcement
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
builder.Configuration.Bind("AzureAd", options);
// Handle claims challenges from CAE
options.Events = new JwtBearerEvents
{
OnChallenge = async context =>
{
// Extract claims challenge from WWW-Authenticate header
if (context.AuthenticateFailure is MsalUiRequiredException msalException)
{
var claimsChallenge = msalException.Claims;
context.Response.Headers.Append(
"WWW-Authenticate",
$"Bearer claims=\"{claimsChallenge}\"");
}
}
};
}, options => builder.Configuration.Bind("AzureAd", options));
// Client-side handling of claims challenges
public async Task<HttpResponseMessage> CallApiWithClaimsHandlingAsync(string url)
{
var response = await _httpClient.GetAsync(url);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var wwwAuthenticate = response.Headers.WwwAuthenticate.ToString();
if (wwwAuthenticate.Contains("claims="))
{
// Parse claims and re-acquire token with claims challenge
var claims = ExtractClaims(wwwAuthenticate);
var token = await _tokenAcquisition.GetAccessTokenForUserAsync(
_scopes,
claimsChallenge: claims);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return await _httpClient.GetAsync(url);
}
}
return response;
}
B2C vs B2B Scenarios
Azure AD B2B (Business-to-Business)
For collaborating with external business partners.
// B2B: External users are invited to your tenant
// They authenticate with their home organization
// Appear as "guest" users in your directory
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
builder.Configuration.Bind("AzureAd", options);
// Allow guests
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ValidIssuers = new[]
{
$"https://login.microsoftonline.com/{tenantId}/v2.0",
"https://login.microsoftonline.com/common/v2.0" // For guests
};
}, options => builder.Configuration.Bind("AzureAd", options));
// Check if user is a guest
public bool IsGuestUser(ClaimsPrincipal user)
{
var userType = user.FindFirst("userType")?.Value;
return userType == "Guest";
}
Azure AD B2C (Business-to-Consumer)
For customer-facing applications with social logins and custom sign-up flows.
// appsettings.json for B2C
{
"AzureAdB2C": {
"Instance": "https://yourb2ctenant.b2clogin.com",
"ClientId": "your-b2c-app-client-id",
"Domain": "yourb2ctenant.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn",
"ResetPasswordPolicyId": "B2C_1_PasswordReset",
"EditProfilePolicyId": "B2C_1_EditProfile"
}
}
// Program.cs
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdB2C"));
// Custom claims from B2C user flows
public class UserProfile
{
public string DisplayName { get; set; }
public string GivenName { get; set; }
public string Surname { get; set; }
public string Email { get; set; }
public static UserProfile FromClaims(ClaimsPrincipal user)
{
return new UserProfile
{
DisplayName = user.FindFirst("name")?.Value,
GivenName = user.FindFirst("given_name")?.Value,
Surname = user.FindFirst("family_name")?.Value,
Email = user.FindFirst("emails")?.Value
?? user.FindFirst(ClaimTypes.Email)?.Value
};
}
}
B2B vs B2C Comparison
| Feature | B2B | B2C |
|---|---|---|
| Target Users | Business partners | Consumers |
| User Management | Guest invitations | Self-service sign-up |
| Identity Providers | Partner Entra ID | Social (Google, Facebook), Local |
| Customization | Limited | Full UI customization |
| User Store | Your Entra ID | Separate B2C directory |
| Licensing | Free (50K MAU) | Per active user |
| Use Case | Partner portals | Customer apps |
Token Validation and Refresh
Validating JWT Tokens
public class TokenValidationService
{
private readonly IConfiguration _config;
public async Task<ClaimsPrincipal> ValidateTokenAsync(string token)
{
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
$"https://login.microsoftonline.com/{_config["AzureAd:TenantId"]}/v2.0/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
var openIdConfig = await configManager.GetConfigurationAsync();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"https://login.microsoftonline.com/{_config["AzureAd:TenantId"]}/v2.0",
ValidateAudience = true,
ValidAudience = _config["AzureAd:ClientId"],
ValidateIssuerSigningKey = true,
IssuerSigningKeys = openIdConfig.SigningKeys,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(
token,
validationParameters,
out var validatedToken);
return principal;
}
catch (SecurityTokenException ex)
{
throw new UnauthorizedAccessException("Invalid token", ex);
}
}
}
Token Refresh Patterns
// MSAL handles token refresh automatically
// DO NOT implement manual refresh logic
// β
Correct: Let MSAL handle it
public async Task<string> GetTokenAsync()
{
// AcquireTokenSilent returns cached token if valid,
// or automatically refreshes if expired (using refresh token)
var result = await _msalClient
.AcquireTokenSilent(_scopes, account)
.ExecuteAsync();
return result.AccessToken;
}
// β Wrong: Manual expiry checking
public async Task<string> GetTokenWrongWayAsync()
{
// Don't do this - MSAL handles it
if (_cachedToken.ExpiresOn < DateTimeOffset.UtcNow)
{
// Manual refresh...
}
}
Best Practices
Security Best Practices
// 1. Use managed identities whenever possible
var credential = new DefaultAzureCredential();
// 2. Store secrets in Key Vault, not config
var secretClient = new SecretClient(
new Uri("https://myvault.vault.azure.net/"),
credential);
// 3. Use certificates instead of client secrets for production
var cert = await GetCertificateFromKeyVaultAsync();
// 4. Apply least-privilege RBAC
// Grant "Storage Blob Data Reader" instead of "Contributor"
// 5. Enable Conditional Access policies
// - Require MFA for sensitive apps
// - Block legacy authentication
// - Require compliant devices for admins
// 6. Use short-lived tokens with CAE
// Access tokens: 1 hour default (up to 28h with CAE)
// Refresh tokens: 90 days for web apps
// 7. Validate tokens properly
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
// 8. Block legacy authentication protocols
// IMAP, POP, SMTP bypass MFA
Performance Best Practices
// 1. Use token caching
builder.Services.AddDistributedMemoryCache();
// Or Redis/SQL Server for distributed scenarios
// 2. Reuse HttpClient with proper token handling
builder.Services.AddHttpClient<IApiClient, ApiClient>()
.AddHttpMessageHandler<AuthorizationMessageHandler>();
// 3. Use appropriate token cache scope
// Web apps: per user (session-based)
// Daemons: per application (shared cache)
// 4. Enable CAE for longer-lived tokens
// Reduces token acquisition overhead
Interview Questions
1. What is the difference between system-assigned and user-assigned managed identities?
Answer:
- System-assigned: Created automatically with the Azure resource, tied to its lifecycle. When the resource is deleted, the identity is deleted. Can only be used by that single resource.
- User-assigned: Created as a standalone Azure resource, can be assigned to multiple resources. Lifecycle is independent of the resources using it. Better for scenarios where multiple resources need the same permissions.
When to use each:
- System-assigned: Single resource needing unique identity
- User-assigned: Multiple resources needing same access, or when identity needs to persist beyond resource lifecycle
2. Explain the difference between OAuth 2.0 and OpenID Connect.
Answer:
- OAuth 2.0: Authorization protocol for granting access to resources. Provides access tokens that allow applications to access APIs on behalf of users.
- OpenID Connect: Authentication protocol built on top of OAuth 2.0. Adds an ID token containing user identity claims.
In practice:
- Use OAuth 2.0 when you only need to access APIs
- Use OpenID Connect when you need to authenticate users (sign them in)
- OpenID Connect provides both: ID token (authentication) + access token (authorization)
3. How would you secure an API that needs to call another API on behalf of the user?
Answer: Use the On-Behalf-Of (OBO) flow:
- Client authenticates user and gets access token for your API
- Your API validates the incoming token
- Your API exchanges that token for a new token for the downstream API
- The new token maintains the userβs identity and permissions
// Using Microsoft.Identity.Web
var token = await _tokenAcquisition
.GetAccessTokenForUserAsync(new[] { "api://downstream-api/.default" });
Key points:
- The downstream API sees the original userβs identity
- Permissions are scoped to what the user has access to
- Both APIs must be registered in Entra ID
4. What is Continuous Access Evaluation (CAE)?
Answer: CAE is a feature that enables real-time enforcement of Conditional Access policies by allowing Azure services to invalidate tokens before their expiration time.
Traditional flow:
- Access token valid for 1 hour
- If user is disabled, token still works until expiry
With CAE:
- Resources can challenge tokens in real-time
- User account disabled = immediate access revocation
- Security events trigger re-evaluation
- Token lifetime can be extended to 28 hours (less token refresh overhead)
Events that trigger CAE:
- User account disabled/deleted
- Password changed
- MFA requirement added
- IP location changed (for location policies)
5. When should you use Azure AD B2C vs B2B?
Answer:
Use B2B when:
- Collaborating with business partners
- Partners already have organizational accounts
- You need to maintain control over guest access
- Example: Partner portal, vendor management system
Use B2C when:
- Building consumer-facing applications
- Need self-service sign-up
- Want social identity providers (Google, Facebook)
- Need customized sign-up/sign-in flows
- Example: E-commerce site, mobile app
Key difference: B2B guests are added to your directory, B2C creates a separate customer directory.
6. How do you handle token caching in a distributed application?
Answer:
// 1. Configure distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = config["Redis:ConnectionString"];
});
// 2. Use MSAL with distributed token cache
builder.Services.AddAuthentication()
.AddMicrosoftIdentityWebApp(config)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
Key considerations:
- Web apps: Cache per user (using userβs object ID as key)
- Daemons: Shared cache (single identity)
- Encrypt tokens at rest in cache
- Set appropriate cache expiration
- Handle cache miss gracefully
Key Takeaways
- Prefer managed identities - Eliminate credential management, automatic lifecycle handling
- Use DefaultAzureCredential - Works in all environments (local dev to production)
- Never store secrets in code - Use Key Vault or managed identities
- Token caching is critical - MSAL handles this automatically, but configure distributed caching for web apps
- Apply least-privilege RBAC - Only grant permissions that are absolutely needed
- Enable CAE - Real-time security policy enforcement
- Block legacy authentication - IMAP, POP, SMTP bypass modern auth protections