Authenticate a Blazor (Server OR WebAssembly) app using Azure Entra ID with Certificate-based Client Credential Authentication (App-to-App)

Below is a clean, production-ready guide on how to authenticate a Blazor (Server OR WebAssembly) app using Azure Entra ID with Certificate-based Client Credential Authentication (App-to-App).

This covers:

✔ Using Azure Entra ID
✔ Registering the Blazor app
✔ Configuring certificate-based authentication
✔ Using MSAL
✔ Calling secured APIs
✔ Best practices for production

I’ve included both Blazor Server and Blazor WebAssembly flows where applicable.

1. Understanding the Authentication Flow

Azure Entra ID supports two authentications:

A) User Authentication (OpenID Connect)

When the app signs in users with Azure AD.

B) App Authentication (Client Credential Flow using Certificate)

App authenticates itself using a certificate.

You can combine both:

  • Users sign in using Azure AD

  • App (Blazor) calls downstream APIs using certificate-based client credentials

This is the most common enterprise setup.

2. Azure Setup (Required)

Step 1: Register Two Apps

You need:

(1) Blazor App Registration

For user login.
Redirect URIs:

https://localhost:7177/signin-oidc

Also add logout:

https://localhost:7177/signout-callback-oidc

(2) API or Protected Resource Registration

Or even Microsoft Graph.

This app will have:

  • Client ID

  • Tenant ID

  • Expose API Scopes

  • Certificate-based credentials (Upload Certificate)

3. Upload Certificate to Entra ID

Go to:

Azure Portal → App Registrations → (API App) → Certificates & Secrets → Upload Certificate

  • Upload .cer (public certificate)

  • Private .pfx is stored only in your Blazor app (never upload).

4. Configure Blazor App to Use Certificate Authentication

For Blazor Server, use Microsoft.Identity.Web.

Install

dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.UI

5. Configure appsettings.json

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "yourcompany.com",
  "TenantId": "xxxx-xxxx-xxxx-xxxx",
  "ClientId": "Blazor-App-Client-ID",
  "CallbackPath": "/signin-oidc"
},
"AzureAdClientCert": {
  "ClientId": "API-Client-ID",
  "TenantId": "xxxx-xxxx-xxxx-xxxx",
  "CertificateThumbprint": "YOUR_CERT_THUMBPRINT",
  "Scopes": "api://API-APP-ID/.default"
}

6. Register Authentication in Program.cs (Blazor Server)

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();
builder.Services.AddRazorPages();


// Load certificate from store
X509Certificate2 clientCert = LoadCertificateFromStore(
    builder.Configuration["AzureAdClientCert:CertificateThumbprint"]
);

// MSAL ConfidentialClient using certificate
builder.Services.AddSingleton<IConfidentialClientApplication>(provider =>
{
    return ConfidentialClientApplicationBuilder
        .Create(builder.Configuration["AzureAdClientCert:ClientId"])
        .WithCertificate(clientCert)
        .WithTenantId(builder.Configuration["AzureAdClientCert:TenantId"])
        .Build();
});

// Token service
builder.Services.AddSingleton<TokenAcquisitionService>();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

7. Certificate Loader Utility

private static X509Certificate2 LoadCertificateFromStore(string thumbprint)
{
    using var store = new X509Store(StoreLocation.CurrentUser);
    store.Open(OpenFlags.ReadOnly);

    return store.Certificates
        .Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
        .OfType<X509Certificate2>()
        .FirstOrDefault()
        ?? throw new Exception("Certificate not found");
}

8. Token Acquisition Service (using MSAL)

public class TokenAcquisitionService
{
    private readonly IConfidentialClientApplication _client;
    private readonly IConfiguration _config;

    public TokenAcquisitionService(IConfidentialClientApplication client, IConfiguration config)
    {
        _client = client;
        _config = config;
    }

    public async Task<string> GetAccessTokenAsync()
    {
        var scopes = _config["AzureAdClientCert:Scopes"].Split(' ');
        
        var result = await _client
            .AcquireTokenForClient(scopes)
            .ExecuteAsync();

        return result.AccessToken;
    }
}

9. Calling API from Blazor using Certificate-based Token

public class ApiClient
{
    private readonly TokenAcquisitionService _tokenService;
    private readonly HttpClient _http;

    public ApiClient(TokenAcquisitionService tokenService, IHttpClientFactory httpFactory)
    {
        _tokenService = tokenService;
        _http = httpFactory.CreateClient("Api");
    }

    public async Task<string> GetSecuredDataAsync()
    {
        string token = await _tokenService.GetAccessTokenAsync();

        _http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        return await _http.GetStringAsync("/secured-endpoint");
    }
}

10. For Blazor WebAssembly (WASM)

WASM cannot securely store a certificate → you must use:

✔ Backend API / BFF (Backend-for-Frontend pattern)
✔ Or Azure API Management

The certificate-based authentication must happen on server side, not in browser.

WASM can only do user authentication, not certificate authentication.

11. End-to-End Flow

User Login Flow (OIDC)

User → Blazor App → Azure AD → ID Token → Authenticated in UI

App-to-App Flow (Certificate)

Blazor App → Certificate → Azure AD Token Endpoint
→ Access Token → Call Downstream API

12. Recommended Architecture

[User]
   ↓ OIDC
[Blazor App] (Server)
   ↓ Certificate-based Client Credential
[Azure Entra ID]
   ↓ Access Token
[Protected API]

13. Security Best Practices

✔ Use PFX with password
✔ Store certificate in Azure Key Vault (Recommended)
✔ Rotate certificates every 6–12 months
✔ Never store cert in GitHub
✔ Use .default scope for client credentials