I been following the instructions for Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID which appears to be exactly what I need.
My issue is when the line “app.MapGet("/weather-forecast", () =>” was ran in “MinimalApiJwt” I got a 401 Unauthorized error.
Removing “.RequireAuthorization()“ shows the function runs without this check and adding “(ClaimsPrincipal user)” as a DI value shows user.Identity.IsAuthenticated is false.
My starting point was then this code from GitHub and the changes are very minimal from the sample, note I am using “Microsoft Entra ID” not “Microsoft Entra External ID” .
My Changes
Overall Solution
Updated to use slnx file format
Updated nuget packages
Project BlazorWebAppEntra (program.cs)
Added line “msIdentityOptions.ClientSecret = SecretValue;”
Created a few variables, set via new ReadConfig function
replaced place holders, based on comments for Entra ID
Project MinimalApiJwt (program.cs)
Comment out “.RequireAuthorization()“ as was getting 401
DI “(ClaimsPrincipal user)” so can check user details
Created a few variables, set via new ReadConfig function
replaced place holders, based on comments for Entra ID
Code from “BlazorWebAppEntra” (Program.cs)
using System.Security.Claims;
using Azure.Core;
using Azure.Identity;
using BlazorWebAppEntra.Client.Weather;
using BlazorWebAppEntra.Components;
using BlazorWebAppEntra.Weather;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true);
string ReadConfig(string key) => builder.Configuration[$"AzureAd:{key}"] ?? throw new Exception($"{key} not set");
string TenantID = ReadConfig("TenantId");
string ClientId = ReadConfig("ClientId");
string WebAPIAppClientID = ReadConfig("WebAPIAppClientID");
string WebAPIAppClientBaseURL = ReadConfig("WebAPIAppClientBaseURL");
string SecretValue = ReadConfig("SecretValue");
string Domain = ReadConfig("Domain");
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.ClientId = ClientId;
msIdentityOptions.Domain = Domain;
msIdentityOptions.Instance = "https://login.microsoftonline.com/";
msIdentityOptions.ResponseType = "code";
msIdentityOptions.TenantId = TenantID;
msIdentityOptions.ClientSecret = SecretValue;
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = WebAPIAppClientBaseURL;
configOptions.Scopes = [ WebAPIAppClientID + "/Weather.Get" ];
})
.AddDistributedTokenCaches();
builder.Services.AddDistributedMemoryCache();
builder.Services.Configure(
options =>
{
options.Encrypt = true;
});
builder.Services.AddAuthorization();
builder.Services.AddScoped();
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapGet("/weather-forecast", ([FromServices] IWeatherForecaster WeatherForecaster) =>
{
return WeatherForecaster.GetWeatherForecastAsync();
}).RequireAuthorization();
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(BlazorWebAppEntra.Client._Imports).Assembly);
app.MapGroup("/authentication").MapLoginAndLogout();
app.Run();
Code from “MinimalApiJwt” (Program.cs)
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Options;
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
string ReadConfig(string key) => builder.Configuration[$"AzureAd:{key}"] ?? throw new Exception($"{key} not set");
string TenantID = ReadConfig("TenantId");
string WebAPIAppClientID = ReadConfig("WebAPIAppClientID");
builder.Services.AddAuthentication()
.AddJwtBearer("Bearer", jwtOptions =>
{
jwtOptions.Authority = "https://sts.windows.net/" + TenantID;
jwtOptions.Audience = "api://" + WebAPIAppClientID;
});
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(); // Add NSwag services
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weather-forecast", (ClaimsPrincipal user) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
}); //.RequireAuthorization();
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Entra ID Admin Centre
I don't have direct access to this as setup by network admin, but if any details are needed, please let me know and I can request them
Thanks for any help in advance