Skip to content

Commit 20beb2a

Browse files
authored
BWA+Entra+BFF sample app (#493)
1 parent 9ec7e37 commit 20beb2a

File tree

100 files changed

+60927
-24
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+60927
-24
lines changed

8.0/BlazorWebAppOidc/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ If you need to open an issue that pertains to the coding of the sample app, open
1717

1818
## Configure the sample
1919

20-
Configure the OIDC provider using the comments in the Program.cs file.
20+
Configure the OIDC provider using the comments in the `Program.cs` file.
2121

2222
## Run the sample
2323

8.0/BlazorWebAppOidcBff/MinimalApiJwt/Program.cs

+16-8
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66
builder.Services.AddAuthentication()
77
.AddJwtBearer("Bearer", jwtOptions =>
88
{
9-
// The following should match the authority configured for the OIDC handler in BlazorWebAppOidc/Program.cs.
10-
// {TENANT ID} is the directory (tenant) ID. If using an ME-ID tenant type, the authority should match
11-
// the issurer (`iss`) of the JWT returned by the identity provider:
12-
// https://sts.windows.net/{TENANT ID}/
13-
jwtOptions.Authority = "https://login.microsoftonline.com/{TENANT ID}/v2.0/";
9+
// The following should match the authority configured for the OIDC handler in BlazorWebAppEntraBff/Program.cs.
10+
// {TENANT ID} is the directory (tenant) ID.
11+
//
12+
// Authority format {AUTHORITY} matches the issurer (`iss`) of the JWT returned by the identity provider.
13+
//
14+
// Authority format {AUTHORITY} for ME-ID tenant type: https://sts.windows.net/{TENANT ID}/
15+
// Authority format {AUTHORITY} for B2C tenant type: https://login.microsoftonline.com/{TENANT ID}/v2.0/
16+
//
17+
//jwtOptions.Audience = "{AUTHORITY}";
18+
//
1419
// The following should match just the path of the Application ID URI configured when adding the "Weather.Get" scope
1520
// under "Expose an API" in the Azure or Entra portal. {CLIENT ID} is the application (client) ID of this
16-
// app's registration in the Azure portal. If using an ME-ID tenant type, the format of the App ID URI is:
17-
// api://{CLIENT ID}
18-
jwtOptions.Audience = "https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}";
21+
// app's registration in the Azure portal.
22+
//
23+
// Audience format {AUDIENCE} for ME-ID tenant type: api://{CLIENT ID}
24+
// Audience format {AUDIENCE} for B2C tenant type: https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
25+
//
26+
//jwtOptions.Audience = "{AUDIENCE}";
1927
});
2028
builder.Services.AddAuthorization();
2129

8.0/BlazorWebAppOidcBff/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This sample features:
77
server and client Blazor apps respectively to capture authentication state and flow it between the server and client.
88
- OIDC authentication with Microsoft Entra without using Entra-specific packages.
99
- The goal is that this sample can be used as a starting point for any OIDC authentication flow.
10-
- A minimal API backend using the JwtBearerHandler to validate JWT tokens saved by the Blazor app in the sign-in cookie.
10+
- A minimal API backend using the `JwtBearerHandler` to validate JWT tokens saved by the Blazor app in the sign-in cookie.
1111
- The BFF pattern using Aspire service discovery and YARP for proxying the requests to `/weatherforecast` on the backend with the `access_token` stored in the cookie.
1212
- Automatic non-interactive token refresh with the help of a custom `CookieOidcRefresher`.
1313

@@ -19,7 +19,7 @@ If you need to open an issue that pertains to the coding of the sample app, open
1919

2020
## Configure the sample
2121

22-
Configure the OIDC provider using the comments in the Program.cs file and guidance in [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](https://learn.microsoft.com/aspnet/core/blazor/security/blazor-web-app-with-oidc?view=aspnetcore-8.0&pivots=with-bff-pattern).
22+
Configure the OIDC provider using the comments in the `Program.cs` file and guidance in [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](https://learn.microsoft.com/aspnet/core/blazor/security/blazor-web-app-with-oidc?view=aspnetcore-8.0&pivots=with-bff-pattern).
2323

2424
## Run the sample
2525

8.0/BlazorWebAppOidcServer/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ If you need to open an issue that pertains to the coding of the sample app, open
1515

1616
## Configure the sample
1717

18-
Configure the OIDC provider using the comments in the Program.cs file.
18+
Configure the OIDC provider using the comments in the `Program.cs` file.
1919

2020
## Run the sample
2121

9.0/BlazorWebAppEntraBff/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Permit the solution file for this sample app
2+
!*.sln
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
3+
4+
<PropertyGroup>
5+
<OutputType>Exe</OutputType>
6+
<TargetFramework>net9.0</TargetFramework>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<IsAspireHost>true</IsAspireHost>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\BlazorWebAppEntra\BlazorWebAppEntra.csproj" />
14+
<ProjectReference Include="..\..\MinimalApiJwt\MinimalApiJwt.csproj" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
19+
</ItemGroup>
20+
21+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.IdentityModel.Logging;
2+
3+
var builder = DistributedApplication.CreateBuilder(args);
4+
5+
var weatherApi = builder.AddProject<Projects.MinimalApiJwt>("weatherapi");
6+
7+
builder.AddProject<Projects.BlazorWebAppEntra>("blazorfrontend")
8+
.WithReference(weatherApi);
9+
10+
builder.Build().Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "http://localhost:15053",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development",
11+
"DOTNET_ENVIRONMENT": "Development",
12+
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16258",
13+
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
14+
}
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsAspireSharedProject>true</IsAspireSharedProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
13+
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0" />
14+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
15+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
16+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
17+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
18+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
19+
</ItemGroup>
20+
21+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Diagnostics.HealthChecks;
5+
using Microsoft.Extensions.Logging;
6+
using OpenTelemetry;
7+
using OpenTelemetry.Metrics;
8+
using OpenTelemetry.Trace;
9+
10+
namespace Microsoft.Extensions.Hosting;
11+
12+
public static class Extensions
13+
{
14+
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
15+
{
16+
builder.ConfigureOpenTelemetry();
17+
18+
builder.AddDefaultHealthChecks();
19+
20+
builder.Services.AddServiceDiscovery();
21+
22+
builder.Services.ConfigureHttpClientDefaults(http =>
23+
{
24+
// Turn on resilience by default
25+
http.AddStandardResilienceHandler();
26+
27+
// Turn on service discovery by default
28+
http.AddServiceDiscovery();
29+
});
30+
31+
return builder;
32+
}
33+
34+
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
35+
{
36+
builder.Logging.AddOpenTelemetry(logging =>
37+
{
38+
logging.IncludeFormattedMessage = true;
39+
logging.IncludeScopes = true;
40+
});
41+
42+
builder.Services.AddOpenTelemetry()
43+
.WithMetrics(metrics =>
44+
{
45+
metrics.AddAspNetCoreInstrumentation()
46+
.AddHttpClientInstrumentation()
47+
.AddRuntimeInstrumentation();
48+
})
49+
.WithTracing(tracing =>
50+
{
51+
tracing.AddAspNetCoreInstrumentation()
52+
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
53+
//.AddGrpcClientInstrumentation()
54+
.AddHttpClientInstrumentation();
55+
});
56+
57+
builder.AddOpenTelemetryExporters();
58+
59+
return builder;
60+
}
61+
62+
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
63+
{
64+
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
65+
66+
if (useOtlpExporter)
67+
{
68+
builder.Services.AddOpenTelemetry().UseOtlpExporter();
69+
}
70+
71+
// Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
72+
// builder.Services.AddOpenTelemetry()
73+
// .WithMetrics(metrics => metrics.AddPrometheusExporter());
74+
75+
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
76+
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
77+
//{
78+
// builder.Services.AddOpenTelemetry()
79+
// .UseAzureMonitor();
80+
//}
81+
82+
return builder;
83+
}
84+
85+
public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
86+
{
87+
builder.Services.AddHealthChecks()
88+
// Add a default liveness check to ensure app is responsive
89+
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
90+
91+
return builder;
92+
}
93+
94+
public static WebApplication MapDefaultEndpoints(this WebApplication app)
95+
{
96+
// Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
97+
// app.MapPrometheusScrapingEndpoint();
98+
99+
// Adding health checks endpoints to applications in non-development environments has security implications.
100+
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
101+
if (app.Environment.IsDevelopment())
102+
{
103+
// All health checks must pass for app to be considered ready to accept traffic after starting
104+
app.MapHealthChecks("/health");
105+
106+
// Only health checks tagged with the "live" tag must pass for app to be considered alive
107+
app.MapHealthChecks("/alive", new HealthCheckOptions
108+
{
109+
Predicate = r => r.Tags.Contains("live")
110+
});
111+
}
112+
113+
return app;
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
8+
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
13+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
14+
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
15+
</ItemGroup>
16+
17+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@implements IDisposable
2+
@inject NavigationManager Navigation
3+
4+
<div class="nav-item px-3">
5+
<AuthorizeView>
6+
<Authorized>
7+
<form action="authentication/logout" method="post">
8+
<AntiforgeryToken />
9+
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
10+
<button type="submit" class="nav-link">
11+
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
12+
</button>
13+
</form>
14+
</Authorized>
15+
<NotAuthorized>
16+
<a class="nav-link" href="authentication/login">
17+
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> Login
18+
</a>
19+
</NotAuthorized>
20+
</AuthorizeView>
21+
</div>
22+
23+
@code {
24+
private string? currentUrl;
25+
26+
protected override void OnInitialized()
27+
{
28+
currentUrl = Navigation.ToBaseRelativePath(Navigation.Uri);
29+
Navigation.LocationChanged += OnLocationChanged;
30+
}
31+
32+
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
33+
{
34+
currentUrl = Navigation.ToBaseRelativePath(e.Location);
35+
StateHasChanged();
36+
}
37+
38+
public void Dispose()
39+
{
40+
Navigation.LocationChanged -= OnLocationChanged;
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.bi {
2+
display: inline-block;
3+
position: relative;
4+
width: 1.25rem;
5+
height: 1.25rem;
6+
margin-right: 0.75rem;
7+
top: -1px;
8+
background-size: cover;
9+
}
10+
11+
.bi-person-badge-nav-menu {
12+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E");
13+
}
14+
15+
.bi-arrow-bar-left-nav-menu {
16+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E");
17+
}
18+
19+
.nav-item {
20+
font-size: 0.9rem;
21+
padding-bottom: 0.5rem;
22+
}
23+
24+
.nav-item .nav-link {
25+
color: #d7d7d7;
26+
background: none;
27+
border: none;
28+
border-radius: 4px;
29+
height: 3rem;
30+
display: flex;
31+
align-items: center;
32+
line-height: 3rem;
33+
width: 100%;
34+
}
35+
36+
.nav-item .nav-link:hover {
37+
background-color: rgba(255,255,255,0.1);
38+
color: white;
39+
}

0 commit comments

Comments
 (0)