Skip to content

Commit 761ec6b

Browse files
committed
Passing tokens in interactive BWAs
1 parent 77e9869 commit 761ec6b

File tree

2 files changed

+194
-37
lines changed

2 files changed

+194
-37
lines changed

aspnetcore/blazor/components/httpcontext.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ uid: blazor/components/httpcontext
1616

1717
<xref:Microsoft.AspNetCore.Http.IHttpContextAccessor> generally should be avoided with interactive rendering because a valid <xref:Microsoft.AspNetCore.Http.HttpContext> isn't always available.
1818

19-
<xref:Microsoft.AspNetCore.Http.IHttpContextAccessor> can be used for components that are statically rendered on the server. **However, we recommend avoiding it if possible.**
19+
<xref:Microsoft.AspNetCore.Http.IHttpContextAccessor> can be used for components that are statically rendered on the server. **However, we recommend avoiding it if possible.** Another valid use case for static server-side rendering is [passing tokens to a server-side app](xref:blazor/security/additional-scenarios#pass-tokens-to-a-server-side-blazor-app).
2020

2121
<xref:Microsoft.AspNetCore.Http.HttpContext> can be used as a [cascading parameter](xref:Microsoft.AspNetCore.Components.CascadingParameterAttribute) only in *statically-rendered root components* for general tasks, such as inspecting and modifying headers or other properties in the `App` component (`Components/App.razor`). The value is always `null` for interactive rendering.
2222

@@ -32,7 +32,7 @@ For additional context in *advanced* edge cases&dagger;, see the discussion in t
3232
* [HttpContext is valid in Interactive Server Rendering Blazor page (`dotnet/AspNetCore.Docs` #34301)](https://github.com/dotnet/AspNetCore.Docs/issues/34301)
3333
* [Security implications of using IHttpContextAccessor in Blazor Server (`dotnet/aspnetcore` #45699)](https://github.com/dotnet/aspnetcore/issues/45699)
3434

35-
&dagger;Most developers building and maintaining Blazor apps don't need to delve into advanced concepts as long as the general guidance in this article is followed.
35+
&dagger;Most developers building and maintaining Blazor apps don't need to delve into advanced concepts when the general guidance in this article is followed.
3636

3737
:::moniker-end
3838

aspnetcore/blazor/security/blazor-web-app-with-oidc.md

Lines changed: 192 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,16 @@ The following specification is covered:
270270
* The Blazor Web App uses [the Server render mode with global interactivity](xref:blazor/components/render-modes).
271271
* This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) or [Microsoft Identity Web](/entra/msal/dotnet/microsoft-identity-web/) packages, nor does the sample app require [Microsoft Azure](https://azure.microsoft.com/) hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.
272272
* Automatic non-interactive token refresh.
273+
* A separate web API project demonstrates a secure web API call for weather data.
273274

274275
For an alternative experience using [Microsoft Authentication Library for .NET](/entra/msal/dotnet/), [Microsoft Identity Web](/entra/msal/dotnet/microsoft-identity-web/), and [Microsoft Entra ID](https://www.microsoft.com/security/business/identity-access/microsoft-entra-id), see <xref:blazor/security/blazor-web-app-entra>.
275276

276277
## Sample app
277278

278-
The sample app consists of a single server-side Blazor Web App project (`BlazorWebAppOidcServer`).
279+
The sample app consists of the following projects:
280+
281+
* `BlazorWebAppOidc`: Blazor Web App server-side project (global Interactive Server rendering).
282+
* `MinimalApiJwt`: Backend web API with a [Minimal API](xref:fundamentals/minimal-apis) endpoint for weather data.
279283

280284
Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppOidcServer` folder for .NET 8 or later.
281285

@@ -311,7 +315,7 @@ dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SE
311315

312316
If using Visual Studio, you can confirm the secret is set by right-clicking the project in **Solution Explorer** and selecting **Manage User Secrets**.
313317

314-
### Configure the app
318+
### Configure the `BlazorWebAppOidcServer` project
315319

316320
The following <xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions> configuration is found in the project's `Program` file on the call to <xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A>:
317321

@@ -337,6 +341,35 @@ The following <xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConn
337341
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
338342
```
339343

344+
* Configure the `Weather.Get` scope for accessing the external web API for weather data. The following example is based on using Entra ID in an ME-ID tenant domain. In the following example, the `{APP ID URI}` placeholder is found in the Entra or Azure portal where the web API is exposed. For any other identity provider, use the appropriate scope.
345+
346+
```csharp
347+
oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");
348+
```
349+
350+
Placeholders in the following examples:
351+
352+
* Directory Name (`{DIRECTORY NAME}`): `contoso`
353+
* Application (Client) Id (`{CLIENT ID}`): `00001111-aaaa-2222-bbbb-3333cccc4444`
354+
355+
The format of the scope depends on the type of tenant in use:
356+
357+
* ME-ID tenant App ID URI (`{APP ID URI}`): `api://{CLIENT ID}`
358+
359+
Example:
360+
361+
```csharp
362+
oidcOptions.Scope.Add("api://00001111-aaaa-2222-bbbb-3333cccc4444/Weather.Get");
363+
```
364+
365+
* AAD B2C tenant App ID URI (`{APP ID URI}`): `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`:
366+
367+
Example:
368+
369+
```csharp
370+
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444/Weather.Get");
371+
```
372+
340373
* <xref:Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.SaveTokens%2A>: Defines whether access and refresh tokens should be stored in the <xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties> after a successful authorization. This property is set to `true` so the refresh token gets stored for non-interactive token refresh.
341374

342375
```csharp
@@ -445,12 +478,131 @@ The following <xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConn
445478
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;
446479
```
447480

448-
## Sample app code
481+
### Configure the `MinimalApiJwt` project
449482

450-
Inspect the sample app for the following features:
483+
Configure the project in the <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions> of the <xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A> call in the project's `Program` file.
451484

452-
* Automatic non-interactive token refresh with the help of a custom cookie refresher (`CookieOidcRefresher.cs`).
453-
* The `Weather` component uses the [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) to prevent unauthorized access. For more information on requiring authorization across the app via an [authorization policy](xref:security/authorization/policies) and opting out of authorization at a subset of public endpoints, see the [Razor Pages OIDC guidance](xref:security/authentication/configure-oidc-web-authentication#force-authorization). For more information on how this app secures weather data, see [Secure data in Blazor Web Apps with Interactive Auto rendering](xref:blazor/security/index#secure-data-in-blazor-web-apps-with-interactive-auto-rendering).
485+
The <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A> sets the Authority for making OIDC calls. We recommend using a separate app registration for the `MinimalApiJwt` project. The authority matches the issurer (`iss`) of the JWT returned by the identity provider.
486+
487+
```csharp
488+
jwtOptions.Authority = "{AUTHORITY}";
489+
```
490+
491+
The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee`:
492+
493+
* ME-ID tenant Authority (`{AUTHORITY}`):
494+
495+
```csharp
496+
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
497+
```
498+
499+
* AAD B2C tenant Authority (`{AUTHORITY}`): `https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/`
500+
501+
```csharp
502+
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
503+
```
504+
505+
The <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A> sets the Audience for any received OIDC token.
506+
507+
```csharp
508+
jwtOptions.Audience = "{APP ID URI}";
509+
```
510+
511+
> [!NOTE]
512+
> When using Microsoft Entra ID, match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal.
513+
514+
The format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Client ID (`{CLIENT ID}`) of `00001111-aaaa-2222-bbbb-3333cccc4444`:
515+
516+
* ME-ID tenant App ID URI (`{APP ID URI}`): `api://{CLIENT ID}`.
517+
518+
```csharp
519+
jwtOptions.Audience = "api://00001111-aaaa-2222-bbbb-3333cccc4444";
520+
```
521+
522+
* AAD B2C tenant App ID URI (`{APP ID URI}`): `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`. The following example uses a directory name (`{DIRECTORY NAME}`) of `contoso`.
523+
524+
```csharp
525+
jwtOptions.Audience = "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444";
526+
```
527+
528+
## Sample solution code
529+
530+
Inspect the sample solution for the following features.
531+
532+
### `MinimalApiJwt` project
533+
534+
The project creates a [Minimal API](xref:fundamentals/minimal-apis) endpoint for weather data:
535+
536+
```csharp
537+
app.MapGet("/weather-forecast", () =>
538+
{
539+
var forecast = Enumerable.Range(1, 5).Select(index =>
540+
new WeatherForecast
541+
(
542+
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
543+
Random.Shared.Next(-20, 55),
544+
summaries[Random.Shared.Next(summaries.Length)]
545+
))
546+
.ToArray();
547+
return forecast;
548+
}).RequireAuthorization();
549+
```
550+
551+
The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see <xref:test/http-files>.
552+
553+
### `BlazorWebAppOidc` project
554+
555+
Automatic non-interactive token refresh is managed by a custom cookie refresher (`CookieOidcRefresher.cs`).
556+
557+
A <xref:System.Net.Http.DelegatingHandler> (`TokenHandler`) manages attaching a user's access token to outgoing requests. The token handler only executes during static server-side rendering (static SSR), so using <xref:Microsoft.AspNetCore.Http.HttpContext> is safe in this scenario. For more information, see <xref:blazor/components/httpcontext> and <xref:blazor/security/additional-scenarios#pass-tokens-to-a-server-side-blazor-app>.
558+
559+
`TokenHandler.cs`:
560+
561+
```csharp
562+
public class TokenHandler(IHttpContextAccessor httpContextAccessor) :
563+
DelegatingHandler
564+
{
565+
protected override async Task<HttpResponseMessage> SendAsync(
566+
HttpRequestMessage request, CancellationToken cancellationToken)
567+
{
568+
var accessToken = httpContextAccessor.HttpContext?
569+
.GetTokenAsync("access_token").Result ??
570+
throw new Exception("No access token");
571+
572+
request.Headers.Authorization =
573+
new AuthenticationHeaderValue("Bearer", accessToken);
574+
575+
return await base.SendAsync(request, cancellationToken);
576+
}
577+
}
578+
```
579+
580+
In the project's `Program` file, the token handler (`TokenHandler`) is registered as a service and specified as the message handler with <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler%2A> for making secure requests to the backend `MinimalApiJwt` web API using a [named HTTP client](xref:blazor/call-web-api#named-httpclient-with-ihttpclientfactory) ("`ExternalApi`").
581+
582+
```csharp
583+
builder.Services.AddScoped<TokenHandler>();
584+
585+
builder.Services.AddHttpClient("ExternalApi",
586+
client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ??
587+
throw new Exception("Missing base address!")))
588+
.AddHttpMessageHandler<TokenHandler>();
589+
```
590+
591+
The `Weather` component uses the [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) to prevent unauthorized access. For more information on requiring authorization across the app via an [authorization policy](xref:security/authorization/policies) and opting out of authorization at a subset of public endpoints, see the [Razor Pages OIDC guidance](xref:security/authentication/configure-oidc-web-authentication#force-authorization).
592+
593+
The `ExternalApi` HTTP client is used to make a request for weather data to the secure web API. In the [`OnInitializedAsync` lifecycle event](xref:blazor/components/lifecycle#component-initialization-oninitializedasync) of `Weather.razor`:
594+
595+
```csharp
596+
var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
597+
var client = ClientFactory.CreateClient("ExternalApi");
598+
599+
var response = await client.SendAsync(request);
600+
601+
response.EnsureSuccessStatusCode();
602+
603+
forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
604+
throw new IOException("No weather forecast!");
605+
```
454606

455607
:::zone-end
456608

@@ -754,56 +906,49 @@ The `MinimalApiJwt.http` file can be used for testing the weather data request.
754906

755907
### Configuration
756908

757-
Configure the project in the <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions> of the <xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A> call in the project's `Program` file:
909+
Configure the project in the <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions> of the <xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A> call in the project's `Program` file.
758910

759-
* <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A>: Sets the Audience for any received OIDC token.
911+
The <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A> sets the Authority for making OIDC calls. We recommend using a separate app registration for the `MinimalApiJwt` project. The authority matches the issurer (`iss`) of the JWT returned by the identity provider.
760912

761-
```csharp
762-
jwtOptions.Audience = "{APP ID URI}";
763-
```
764-
765-
> [!NOTE]
766-
> When using Microsoft Entra ID, match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal.
913+
```csharp
914+
jwtOptions.Authority = "{AUTHORITY}";
915+
```
767916

768-
Example:
917+
The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee`:
769918

770-
App ID URI (`{APP ID URI}`): `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`:
771-
772-
* Directory Name (`{DIRECTORY NAME}`): `contoso`
773-
* Application (Client) Id (`{CLIENT ID}`): `00001111-aaaa-2222-bbbb-3333cccc4444`
919+
* ME-ID tenant Authority (`{AUTHORITY}`):
774920

775921
```csharp
776-
jwtOptions.Audience = "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444";
922+
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
777923
```
778924

779-
The preceding example pertains to an app registered in a tenant with an AAD B2C tenant type. If the app is registered in an ME-ID tenant, the App ID URI is different, thus the audience is different.
780-
781-
Example:
782-
783-
App ID URI (`{APP ID URI}`): `api://{CLIENT ID}` with Application (Client) Id (`{CLIENT ID}`): `00001111-aaaa-2222-bbbb-3333cccc4444`
925+
* AAD B2C tenant Authority (`{AUTHORITY}`): `https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/`
784926

785927
```csharp
786-
jwtOptions.Audience = "api://00001111-aaaa-2222-bbbb-3333cccc4444";
928+
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
787929
```
788930

789-
* <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A>: Sets the Authority for making OIDC calls. Match the value to the Authority configured for the OIDC handler in `BlazorWebAppOidc/Program.cs`:
931+
The <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A> sets the Audience for any received OIDC token.
790932

791-
```csharp
792-
jwtOptions.Authority = "{AUTHORITY}";
793-
```
933+
```csharp
934+
jwtOptions.Audience = "{APP ID URI}";
935+
```
794936

795-
Example:
937+
> [!NOTE]
938+
> When using Microsoft Entra ID, match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal.
939+
940+
The format of the Audience depends on the type of tenant in use. The following examples use a Client ID (`{CLIENT ID}`) of `00001111-aaaa-2222-bbbb-3333cccc4444`:
796941

797-
Authority (`{AUTHORITY}`): `https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/` (uses Tenant ID `aaaabbbb-0000-cccc-1111-dddd2222eeee`)
942+
* ME-ID tenant App ID URI (`{APP ID URI}`): `api://{CLIENT ID}`.
798943

799944
```csharp
800-
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
945+
jwtOptions.Audience = "api://00001111-aaaa-2222-bbbb-3333cccc4444";
801946
```
802947

803-
The preceding example pertains to an app registered in a tenant with an AAD B2C tenant type. If the app is registered in an ME-ID tenant, the authority should match the issurer (`iss`) of the JWT returned by the identity provider:
948+
* AAD B2C tenant App ID URI (`{APP ID URI}`): `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`. The following example uses a directory name (`{DIRECTORY NAME}`) of `contoso`.
804949

805950
```csharp
806-
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
951+
jwtOptions.Audience = "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444";
807952
```
808953

809954
### Minimal API for weather data
@@ -829,6 +974,18 @@ The <xref:Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExt
829974

830975
:::zone-end
831976

977+
## Microsoft Entra ID app registrations
978+
979+
We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution.
980+
981+
When using Microsoft Entra ID, grant API permission to the app (`BlazorWebAppOidcServer`) to access the web API (`MinimalApiJwt`):
982+
983+
* The web API's (`MinimalApiJwt`) registration exposes its API in **App registrations** > **Expose an API**.
984+
985+
* The app (`BlazorWebAppOidcServer`) registration grants users delegated access to the web API in **App registrations** > **API permissions**. Grant admin consent for the organization to access the web API.
986+
987+
* Authorized users and groups are assigned to the app's (`BlazorWebAppOidcServer`) registration in **Enterprise applications**.
988+
832989
## Redirect to the home page on logout
833990

834991
The `LogInOrOut` component (`Layout/LogInOrOut.razor`) sets a hidden field for the return URL (`ReturnUrl`) to the current URL (`currentURL`). When the user signs out of the app, the identity provider returns the user to the page from which they logged out. If the user logs out from a secure page, they're returned to the same secure page and sent back through the authentication process. This authentication flow is reasonable when users need to change accounts regularly.

0 commit comments

Comments
 (0)