☁️

Azure Authentication Identity

Cloud & Azure Intermediate 7 min read 1300 words

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

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:

  1. Client authenticates user and gets access token for your API
  2. Your API validates the incoming token
  3. Your API exchanges that token for a new token for the downstream API
  4. 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

  1. Prefer managed identities - Eliminate credential management, automatic lifecycle handling
  2. Use DefaultAzureCredential - Works in all environments (local dev to production)
  3. Never store secrets in code - Use Key Vault or managed identities
  4. Token caching is critical - MSAL handles this automatically, but configure distributed caching for web apps
  5. Apply least-privilege RBAC - Only grant permissions that are absolutely needed
  6. Enable CAE - Real-time security policy enforcement
  7. Block legacy authentication - IMAP, POP, SMTP bypass modern auth protections

Further Reading

πŸ“š Related Articles