Skip to content

Commit 34cdfe4

Browse files
authored
Add call web API to BWA+OIDC samples (#526)
1 parent b183e6a commit 34cdfe4

32 files changed

+408
-68
lines changed

8.0/BlazorWebAppOidc/BlazorWebAppOidc.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWebAppOidc", "BlazorW
77
EndProject
88
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWebAppOidc.Client", "BlazorWebAppOidc.Client\BlazorWebAppOidc.Client.csproj", "{7B3838A1-D145-479C-9B79-6D2CD1775B71}"
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalApiJwt", "..\GitHub\blazor-samples\9.0\BlazorWebAppEntra\MinimalApiJwt\MinimalApiJwt.csproj", "{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
2123
{7B3838A1-D145-479C-9B79-6D2CD1775B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
2224
{7B3838A1-D145-479C-9B79-6D2CD1775B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
2325
{7B3838A1-D145-479C-9B79-6D2CD1775B71}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Release|Any CPU.Build.0 = Release|Any CPU
2430
EndGlobalSection
2531
GlobalSection(SolutionProperties) = preSolution
2632
HideSolutionNode = FALSE
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"Name": "Start Projects",
4+
"Projects": [
5+
{
6+
"Path": "MinimalApiJwt\\MinimalApiJwt.csproj",
7+
"Action": "StartWithoutDebugging",
8+
"DebugTarget": "https"
9+
},
10+
{
11+
"Path": "BlazorWebAppOidc\\BlazorWebAppOidc.csproj",
12+
"Action": "Start",
13+
"DebugTarget": "https"
14+
}
15+
]
16+
}
17+
]

8.0/BlazorWebAppOidc/BlazorWebAppOidc/Program.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@
3636
//oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
3737
// ........................................................................
3838

39+
// ........................................................................
40+
// The "Weather.Get" scope for accessing the external web API for weather
41+
// data. The following example is based on using Microsoft Entra ID in
42+
// an ME-ID tenant domain (the {APP ID URI} placeholder is found in
43+
// the Entra or Azure portal where the web API is exposed). For any other
44+
// identity provider, use the appropriate scope.
45+
46+
oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");
47+
// ........................................................................
48+
3949
// ........................................................................
4050
// The following paths must match the redirect and post logout redirect
4151
// paths configured when registering the application with the OIDC provider.
@@ -138,6 +148,13 @@
138148

139149
builder.Services.AddHttpContextAccessor();
140150

151+
builder.Services.AddScoped<TokenHandler>();
152+
153+
builder.Services.AddHttpClient("ExternalApi",
154+
client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ??
155+
throw new Exception("Missing base address!")))
156+
.AddHttpMessageHandler<TokenHandler>();
157+
141158
var app = builder.Build();
142159

143160
// Configure the HTTP request pipeline.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Net.Http.Headers;
2+
using Microsoft.AspNetCore.Authentication;
3+
4+
namespace BlazorWebAppOidc;
5+
6+
public class TokenHandler(IHttpContextAccessor httpContextAccessor) :
7+
DelegatingHandler
8+
{
9+
protected override async Task<HttpResponseMessage> SendAsync(
10+
HttpRequestMessage request, CancellationToken cancellationToken)
11+
{
12+
var accessToken = httpContextAccessor.HttpContext?
13+
.GetTokenAsync("access_token").Result ??
14+
throw new Exception("No access token");
15+
16+
request.Headers.Authorization =
17+
new AuthenticationHeaderValue("Bearer", accessToken);
18+
19+
return await base.SendAsync(request, cancellationToken);
20+
}
21+
}

8.0/BlazorWebAppOidc/BlazorWebAppOidc/Weather/ServerWeatherForecaster.cs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,18 @@
22

33
namespace BlazorWebAppOidc.Weather;
44

5-
internal sealed class ServerWeatherForecaster() : IWeatherForecaster
5+
internal sealed class ServerWeatherForecaster(IHttpClientFactory clientFactory) : IWeatherForecaster
66
{
7-
public readonly string[] summaries =
8-
[
9-
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
10-
];
11-
127
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync()
138
{
14-
// Simulate asynchronous loading to demonstrate streaming rendering
15-
await Task.Delay(500);
9+
var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
10+
var client = clientFactory.CreateClient("ExternalApi");
11+
12+
var response = await client.SendAsync(request);
13+
14+
response.EnsureSuccessStatusCode();
1615

17-
return Enumerable.Range(1, 5).Select(index =>
18-
new WeatherForecast
19-
(
20-
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
21-
Random.Shared.Next(-20, 55),
22-
summaries[Random.Shared.Next(summaries.Length)]
23-
))
24-
.ToArray();
16+
return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
17+
throw new IOException("No weather forecast!");
2518
}
2619
}

8.0/BlazorWebAppOidc/BlazorWebAppOidc/appsettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"AllowedHosts": "*"
8+
"AllowedHosts": "*",
9+
"ExternalApiUri": "https://localhost:7277"
910
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<InvariantGlobalization>true</InvariantGlobalization>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@MinimalApiJwt_HostAddress = http://localhost:5163
2+
3+
GET {{MinimalApiJwt_HostAddress}}/weather-forecast/
4+
Accept: application/json
5+
6+
###
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
var builder = WebApplication.CreateBuilder(args);
2+
3+
builder.Services.AddAuthentication()
4+
.AddJwtBearer("Bearer", jwtOptions =>
5+
{
6+
// {TENANT ID} is the directory (tenant) ID.
7+
//
8+
// Authority format {AUTHORITY} matches the issurer (`iss`) of the JWT returned by the identity provider.
9+
//
10+
// Authority format {AUTHORITY} for ME-ID tenant type: https://sts.windows.net/{TENANT ID}/
11+
// Authority format {AUTHORITY} for B2C tenant type: https://login.microsoftonline.com/{TENANT ID}/v2.0/
12+
//
13+
jwtOptions.Authority = "{AUTHORITY}";
14+
//
15+
// The following should match just the path of the Application ID URI configured when adding the "Weather.Get" scope
16+
// under "Expose an API" in the Azure or Entra portal. {CLIENT ID} is the application (client) ID of this
17+
// app's registration in the Azure portal.
18+
//
19+
// Audience format {AUDIENCE} for ME-ID tenant type: api://{CLIENT ID}
20+
// Audience format {AUDIENCE} for B2C tenant type: https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
21+
//
22+
jwtOptions.Audience = "{AUDIENCE}";
23+
});
24+
builder.Services.AddAuthorization();
25+
26+
var app = builder.Build();
27+
28+
// Configure the HTTP request pipeline.
29+
app.UseHttpsRedirection();
30+
31+
var summaries = new[]
32+
{
33+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
34+
};
35+
36+
app.MapGet("/weather-forecast", () =>
37+
{
38+
var forecast = Enumerable.Range(1, 5).Select(index =>
39+
new WeatherForecast
40+
(
41+
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
42+
Random.Shared.Next(-20, 55),
43+
summaries[Random.Shared.Next(summaries.Length)]
44+
))
45+
.ToArray();
46+
return forecast;
47+
}).RequireAuthorization();
48+
49+
app.Run();
50+
51+
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
52+
{
53+
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"https": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"launchUrl": "weather-forecast",
9+
"applicationUrl": "https://localhost:7277;http://localhost:5163",
10+
"environmentVariables": {
11+
"ASPNETCORE_ENVIRONMENT": "Development"
12+
}
13+
}
14+
}
15+
}
Lines changed: 8 additions & 0 deletions
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+
}
Lines changed: 9 additions & 0 deletions
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+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

8.0/BlazorWebAppOidc/README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
This sample features:
44

5-
- A Blazor Web App with global Auto interactivity.
6-
- This adds a `PersistingAuthenticationStateProvider` and `PersistentAuthenticationStateProvider` services to the
7-
server and client Blazor apps respectively to capture authentication state and flow it between the server and client.
8-
- OIDC authentication with Microsoft Entra without using Entra-specific packages.
9-
- The goal is that this sample can be used as a starting point for any OIDC authentication flow.
10-
- Automatic non-interactive token refresh with the help of a custom `CookieOidcRefresher`.
5+
* A Blazor Web App with global Auto interactivity.
6+
* `PersistingAuthenticationStateProvider` and `PersistentAuthenticationStateProvider` services are added to the server and client Blazor apps respectively to capture authentication state and flow it between the server and client.
7+
* OIDC authentication with Microsoft Entra without using Entra-specific packages. This sample can be used as a starting point for any OIDC authentication flow.
8+
* Automatic non-interactive token refresh with the help of a custom `CookieOidcRefresher`.
9+
* Secure web API call for weather data to a separate web API project. The access token is obtained from the server-side `HttpContext` and attached to outgoing requests with a custom `DelegatingHandler` service.
1110

1211
## Article for this sample app
1312

@@ -24,8 +23,11 @@ Configure the OIDC provider using the comments in the `Program.cs` file.
2423
### Visual Studio
2524

2625
1. Open the `BlazorWebAppOidc` solution file in Visual Studio.
27-
1. Select the `BlazorWebAppOidc` project in **Solution Explorer** and start the app with either Visual Studio's Run button or by selecting **Start Debugging** from the **Debug** menu.
26+
1. Use the **Start Projects** launch profile to start the web API app and Blazor apps.
2827

2928
### .NET CLI
3029

31-
In a command shell, navigate to the `BlazorWebAppOidc` project folder and use the `dotnet run` command to run the sample.
30+
In a command shell:
31+
32+
* Navigate to the `MinimalApiJwt` project folder and use the `dotnet run` command to run the project.
33+
* Navigate to the `BlazorWebAppOidc` project folder and use the `dotnet watch` command to run the project.

8.0/BlazorWebAppOidcServer/BlazorWebAppOidcServer.slnLaunch.user

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"Name": "Start Projects",
44
"Projects": [
55
{
6-
"Path": "BlazorWebAppOidcServer\\BlazorWebAppOidcServer.csproj",
7-
"Action": "Start",
6+
"Path": "MinimalApiJwt\\MinimalApiJwt.csproj",
7+
"Action": "StartWithoutDebugging",
88
"DebugTarget": "https"
99
},
1010
{
11-
"Path": "MinimalApiJwt\\MinimalApiJwt.csproj",
12-
"Action": "StartWithoutDebugging",
11+
"Path": "BlazorWebAppOidcServer\\BlazorWebAppOidcServer.csproj",
12+
"Action": "Start",
1313
"DebugTarget": "https"
1414
}
1515
]

8.0/BlazorWebAppOidcServer/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This sample features:
55
* A Blazor Web App with global Server interactivity.
66
* OIDC authentication with Microsoft Entra without using Entra-specific packages. This sample can be used as a starting point for any OIDC authentication flow.
77
* Automatic non-interactive token refresh with the help of a custom `CookieOidcRefresher`.
8-
* Secure web API call for weather data to a separate web API project. The access token is obtained from the server-side `HttpContext` and attached to outgoing requests with a `DelegatingHandler` service.
8+
* Secure web API call for weather data to a separate web API project. The access token is obtained from the server-side `HttpContext` and attached to outgoing requests with a custom `DelegatingHandler` service.
99

1010
## Article for this sample app
1111

@@ -35,5 +35,5 @@ Configure the OIDC provider using the comments in the `Program.cs` file and the
3535

3636
In a command shell:
3737

38-
1. Navigate to the `MinimalApiJwt` project folder and use the `dotnet run` or `dotnet watch` command to run the project.
39-
1. Navigate to the `BlazorWebAppOidcServer` project folder and use the `dotnet run` or `dotnet watch` command to run the project.
38+
1. Navigate to the `MinimalApiJwt` project folder and use the `dotnet run` command to run the project.
39+
1. Navigate to the `BlazorWebAppOidcServer` project folder and use the `dotnet watch` command to run the project.

9.0/BlazorWebAppEntra/BlazorWebAppEntra.slnLaunch.user

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"Name": "Start Projects",
44
"Projects": [
55
{
6-
"Path": "BlazorWebAppEntra\\BlazorWebAppEntra.csproj",
7-
"Action": "Start",
6+
"Path": "MinimalApiJwt\\MinimalApiJwt.csproj",
7+
"Action": "StartWithoutDebugging",
88
"DebugTarget": "https"
99
},
1010
{
11-
"Path": "MinimalApiJwt\\MinimalApiJwt.csproj",
12-
"Action": "StartWithoutDebugging",
11+
"Path": "BlazorWebAppEntra\\BlazorWebAppEntra.csproj",
12+
"Action": "Start",
1313
"DebugTarget": "https"
1414
}
1515
]

9.0/BlazorWebAppOidc/BlazorWebAppOidc.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWebAppOidc", "BlazorW
77
EndProject
88
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWebAppOidc.Client", "BlazorWebAppOidc.Client\BlazorWebAppOidc.Client.csproj", "{7B3838A1-D145-479C-9B79-6D2CD1775B71}"
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalApiJwt", "..\GitHub\blazor-samples\9.0\BlazorWebAppEntra\MinimalApiJwt\MinimalApiJwt.csproj", "{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
2123
{7B3838A1-D145-479C-9B79-6D2CD1775B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
2224
{7B3838A1-D145-479C-9B79-6D2CD1775B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
2325
{7B3838A1-D145-479C-9B79-6D2CD1775B71}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{9C2046C8-7491-5AB5-AAA9-FB558DCE801E}.Release|Any CPU.Build.0 = Release|Any CPU
2430
EndGlobalSection
2531
GlobalSection(SolutionProperties) = preSolution
2632
HideSolutionNode = FALSE
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"Name": "Start Projects",
4+
"Projects": [
5+
{
6+
"Path": "MinimalApiJwt\\MinimalApiJwt.csproj",
7+
"Action": "StartWithoutDebugging",
8+
"DebugTarget": "https"
9+
},
10+
{
11+
"Path": "BlazorWebAppOidc\\BlazorWebAppOidc.csproj",
12+
"Action": "Start",
13+
"DebugTarget": "https"
14+
}
15+
]
16+
}
17+
]

0 commit comments

Comments
 (0)