Skip to content

Commit 307736d

Browse files
authored
Improvements to Blazor auth coverage (#17420)
1 parent a70c1ff commit 307736d

12 files changed

Lines changed: 504 additions & 152 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
## UserManager and SignInManager
2+
3+
Set the user identifier claim type when a Server app requires:
4+
5+
* <xref:Microsoft.AspNetCore.Identity.UserManager%601> or <xref:Microsoft.AspNetCore.Identity.SignInManager%601> in an API endpoint.
6+
* <xref:Microsoft.AspNetCore.Identity.IdentityUser> details, such as the user's name, email address, or lockout end time.
7+
8+
In `Startup.ConfigureServices`:
9+
10+
```csharp
11+
services.Configure<IdentityOptions>(options =>
12+
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
13+
```
14+
15+
The following `WeatherForecastController` logs the <xref:Microsoft.AspNetCore.Identity.IdentityUser%601.UserName> when the `Get` method is called:
16+
17+
```csharp
18+
using System;
19+
using System.Collections.Generic;
20+
using System.Linq;
21+
using System.Threading.Tasks;
22+
using Microsoft.AspNetCore.Authorization;
23+
using Microsoft.AspNetCore.Mvc;
24+
using Microsoft.AspNetCore.Identity;
25+
using Microsoft.Extensions.Logging;
26+
using BlazorAppIdentityServer.Server.Models;
27+
using BlazorAppIdentityServer.Shared;
28+
29+
namespace BlazorAppIdentityServer.Server.Controllers
30+
{
31+
[Authorize]
32+
[ApiController]
33+
[Route("[controller]")]
34+
public class WeatherForecastController : ControllerBase
35+
{
36+
private readonly UserManager<ApplicationUser> _userManager;
37+
38+
private static readonly string[] Summaries = new[]
39+
{
40+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
41+
"Balmy", "Hot", "Sweltering", "Scorching"
42+
};
43+
44+
private readonly ILogger<WeatherForecastController> logger;
45+
46+
public WeatherForecastController(ILogger<WeatherForecastController> logger,
47+
UserManager<ApplicationUser> userManager)
48+
{
49+
this.logger = logger;
50+
_userManager = userManager;
51+
}
52+
53+
[HttpGet]
54+
public async Task<IEnumerable<WeatherForecast>> Get()
55+
{
56+
var rng = new Random();
57+
58+
var user = await _userManager.GetUserAsync(User);
59+
60+
if (user != null)
61+
{
62+
logger.LogInformation($"User.Identity.Name: {user.UserName}");
63+
}
64+
65+
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
66+
{
67+
Date = DateTime.Now.AddDays(index),
68+
TemperatureC = rng.Next(-20, 55),
69+
Summary = Summaries[rng.Next(Summaries.Length)]
70+
})
71+
.ToArray();
72+
}
73+
}
74+
}
75+
```

aspnetcore/security/blazor/index.md

Lines changed: 87 additions & 129 deletions
Large diffs are not rendered by default.

aspnetcore/security/blazor/server.md

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Learn how to mitigate security threats to Blazor Server apps.
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: riande
77
ms.custom: mvc
8-
ms.date: 03/16/2020
8+
ms.date: 03/31/2020
99
no-loc: [Blazor, SignalR]
1010
uid: security/blazor/server
1111
---
@@ -24,6 +24,197 @@ In constrained environments, such as inside corporate networks or intranets, som
2424
* Doesn't apply in the constrained environment.
2525
* Isn't worth the cost to implement because the security risk is low in a constrained environment.
2626

27+
## Blazor Server project template
28+
29+
The Blazor Server project template can be configured for authentication when the project is created.
30+
31+
# [Visual Studio](#tab/visual-studio)
32+
33+
Follow the Visual Studio guidance in the <xref:blazor/get-started> article to create a new Blazor Server project with an authentication mechanism.
34+
35+
After choosing the **Blazor Server App** template in the **Create a new ASP.NET Core Web Application** dialog, select **Change** under **Authentication**.
36+
37+
A dialog opens to offer the same set of authentication mechanisms available for other ASP.NET Core projects:
38+
39+
* **No Authentication**
40+
* **Individual User Accounts** &ndash; User accounts can be stored:
41+
* Within the app using ASP.NET Core's [Identity](xref:security/authentication/identity) system.
42+
* With [Azure AD B2C](xref:security/authentication/azure-ad-b2c).
43+
* **Work or School Accounts**
44+
* **Windows Authentication**
45+
46+
# [Visual Studio Code](#tab/visual-studio-code)
47+
48+
Follow the Visual Studio Code guidance in the <xref:blazor/get-started> article to create a new Blazor Server project with an authentication mechanism:
49+
50+
```dotnetcli
51+
dotnet new blazorserver -o {APP NAME} -au {AUTHENTICATION}
52+
```
53+
54+
Permissible authentication values (`{AUTHENTICATION}`) are shown in the following table.
55+
56+
| Authentication mechanism | `{AUTHENTICATION}` value |
57+
| ---------------------------------------------------------------------------------------- | :----------------------: |
58+
| No Authentication | `None` |
59+
| Individual<br>Users stored in the app with ASP.NET Core Identity. | `Individual` |
60+
| Individual<br>Users stored in [Azure AD B2C](xref:security/authentication/azure-ad-b2c). | `IndividualB2C` |
61+
| Work or School Accounts<br>Organizational authentication for a single tenant. | `SingleOrg` |
62+
| Work or School Accounts<br>Organizational authentication for multiple tenants. | `MultiOrg` |
63+
| Windows Authentication | `Windows` |
64+
65+
The command creates a folder named with the value provided for the `{APP NAME}` placeholder and uses the folder name as the app's name. For more information, see the [dotnet new](/dotnet/core/tools/dotnet-new) command in the .NET Core Guide.
66+
67+
# [Visual Studio for Mac](#tab/visual-studio-mac)
68+
69+
1. Follow the Visual Studio for Mac guidance in the <xref:blazor/get-started> article.
70+
71+
1. On the **Configure your new Blazor Server App** step, select **Individual Authentication (in-app)** from the **Authentication** drop down.
72+
73+
1. The app is created for individual users stored in the app with ASP.NET Core Identity.
74+
75+
# [.NET Core CLI](#tab/netcore-cli/)
76+
77+
Follow the .NET Core CLI guidance in the <xref:blazor/get-started> article to create a new Blazor Server project with an authentication mechanism:
78+
79+
```dotnetcli
80+
dotnet new blazorserver -o {APP NAME} -au {AUTHENTICATION}
81+
```
82+
83+
Permissible authentication values (`{AUTHENTICATION}`) are shown in the following table.
84+
85+
| Authentication mechanism | `{AUTHENTICATION}` value |
86+
| ---------------------------------------------------------------------------------------- | :----------------------: |
87+
| No Authentication | `None` |
88+
| Individual<br>Users stored in the app with ASP.NET Core Identity. | `Individual` |
89+
| Individual<br>Users stored in [Azure AD B2C](xref:security/authentication/azure-ad-b2c). | `IndividualB2C` |
90+
| Work or School Accounts<br>Organizational authentication for a single tenant. | `SingleOrg` |
91+
| Work or School Accounts<br>Organizational authentication for multiple tenants. | `MultiOrg` |
92+
| Windows Authentication | `Windows` |
93+
94+
The command creates a folder named with the value provided for the `{APP NAME}` placeholder and uses the folder name as the app's name. For more information, see the [dotnet new](/dotnet/core/tools/dotnet-new) command in the .NET Core Guide.
95+
96+
---
97+
98+
## Pass tokens to a Blazor Server app
99+
100+
Authenticate the Blazor Server app as you would with a regular Razor Pages or MVC app. Provision and save the tokens to the authentication cookie. For example:
101+
102+
```csharp
103+
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
104+
{
105+
options.ResponseType = "code";
106+
options.SaveTokens = true;
107+
108+
options.Scope.Add("offline_access");
109+
options.Scope.Add("{SCOPE}");
110+
options.Resource = "{RESOURCE}";
111+
});
112+
```
113+
114+
For sample code, including a complete `Startup.ConfigureServices` example, see the [Passing tokens to a server-side Blazor application](https://github.com/javiercn/blazor-server-aad-sample).
115+
116+
Define a class to pass in the initial app state with the access and refresh tokens:
117+
118+
```csharp
119+
public class InitialApplicationState
120+
{
121+
public string AccessToken { get; set; }
122+
public string RefreshToken { get; set; }
123+
}
124+
```
125+
126+
Define a **scoped** token provider service that can be used within the Blazor app to resolve the tokens from DI:
127+
128+
```csharp
129+
using System;
130+
using System.Security.Claims;
131+
using System.Threading.Tasks;
132+
133+
public class TokenProvider
134+
{
135+
public string AccessToken { get; set; }
136+
public string RefreshToken { get; set; }
137+
}
138+
```
139+
140+
In `Startup.ConfigureServices`, register the token provider service:
141+
142+
```csharp
143+
services.AddScoped<TokenProvider>();
144+
```
145+
146+
In the *_Host.cshtml* file, create and instance of `InitialApplicationState` and pass it as a parameter to the app:
147+
148+
```cshtml
149+
@using Microsoft.AspNetCore.Authentication
150+
151+
...
152+
153+
@{
154+
var tokens = new InitialApplicationState
155+
{
156+
AccessToken = await HttpContext.GetTokenAsync("access_token"),
157+
RefreshToken = await HttpContext.GetTokenAsync("refresh_token")
158+
};
159+
}
160+
161+
<app>
162+
<component type="typeof(App)" param-InitialState="tokens"
163+
render-mode="ServerPrerendered" />
164+
</app>
165+
```
166+
167+
In the `App` component (*App.razor*), resolve the service and initialize it with the data from the parameter:
168+
169+
```razor
170+
@inject TokenProvider TokensProvider
171+
172+
...
173+
174+
@code {
175+
[Parameter]
176+
public InitialApplicationState InitialState { get; set; }
177+
178+
protected override Task OnInitializedAsync()
179+
{
180+
TokensProvider.AccessToken = InitialState.AccessToken;
181+
TokensProvider.RefreshToken = InitialState.RefreshToken;
182+
183+
return base.OnInitializedAsync();
184+
}
185+
}
186+
```
187+
188+
In the service that makes a secure API request, inject the token provider and retrieve the token to call the API:
189+
190+
```csharp
191+
public class WeatherForecastService
192+
{
193+
private readonly TokenProvider _store;
194+
195+
public WeatherForecastService(IHttpClientFactory clientFactory,
196+
TokenProvider tokenProvider)
197+
{
198+
Client = clientFactory.CreateClient();
199+
_store = tokenProvider;
200+
}
201+
202+
public HttpClient Client { get; }
203+
204+
public async Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
205+
{
206+
var token = _store.AccessToken;
207+
var request = new HttpRequestMessage(HttpMethod.Get,
208+
"https://localhost:5003/WeatherForecast");
209+
request.Headers.Add("Authorization", $"Bearer {token}");
210+
var response = await Client.SendAsync(request);
211+
response.EnsureSuccessStatusCode();
212+
213+
return await response.Content.ReadAsAsync<WeatherForecast[]>();
214+
}
215+
}
216+
```
217+
27218
## Resource exhaustion
28219

29220
Resource exhaustion can occur when a client interacts with the server and causes the server to consume excessive resources. Excessive resource consumption primarily affects:

aspnetcore/security/blazor/webassembly/additional-scenarios.md

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description:
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: riande
77
ms.custom: mvc
8-
ms.date: 03/19/2020
8+
ms.date: 03/30/2020
99
no-loc: [Blazor, SignalR]
1010
uid: security/blazor/webassembly/additional-scenarios
1111
---
@@ -17,6 +17,45 @@ By [Javier Calvarro Nelson](https://github.com/javiercn)
1717

1818
[!INCLUDE[](~/includes/blazorwasm-3.2-template-article-notice.md)]
1919

20+
## Request additional access tokens
21+
22+
Most apps only require an access token to interact with the protected resources that they use. In some scenarios, an app might require more than one token in order to interact with two or more resources.
23+
24+
In the following example, additional Azure Active Directory (AAD) Microsoft Graph API scopes are required by an app to read user data and send mail. After adding the Microsoft Graph API permissions in the Azure AAD portal, the additional scopes are configured in the Client app (`Program.Main`, *Program.cs*):
25+
26+
```csharp
27+
builder.Services.AddMsalAuthentication(options =>
28+
{
29+
...
30+
31+
options.ProviderOptions.AdditionalScopesToConsent.Add(
32+
"https://graph.microsoft.com/Mail.Send");
33+
options.ProviderOptions.AdditionalScopesToConsent.Add(
34+
"https://graph.microsoft.com/User.Read");
35+
}
36+
```
37+
38+
The `IAccessTokenProvider.RequestToken` method provides an overload that allows an app to provision a token with a given set of scopes, as seen in the following example:
39+
40+
```csharp
41+
var tokenResult = await AuthenticationService.RequestAccessToken(
42+
new AccessTokenRequestOptions
43+
{
44+
Scopes = new[] { "https://graph.microsoft.com/Mail.Send",
45+
"https://graph.microsoft.com/User.Read" }
46+
});
47+
48+
if (tokenResult.TryGetToken(out var token))
49+
{
50+
...
51+
}
52+
```
53+
54+
`TryGetToken` returns:
55+
56+
* `true` with the `token` for use.
57+
* `false` if the token isn't retrieved.
58+
2059
## Handle token request errors
2160

2261
When a Single Page Application (SPA) authenticates a user using Open ID Connect (OIDC), the authentication state is maintained locally within the SPA and in the Identity Provider (IP) in the form of a session cookie that's set as a result of the user providing their credentials.
@@ -155,19 +194,6 @@ During an authentication operation, there are cases where you want to save the a
155194
}
156195
```
157196

158-
## Request additional access tokens
159-
160-
Most apps only require an access token to interact with the protected resources that they use. In some scenarios, an app might require more than one token in order to interact with two or more resources. The `IAccessTokenProvider.RequestToken` method provides an overload that allows an app to provision a token with a given set of scopes, as seen in the following example:
161-
162-
```csharp
163-
var tokenResult = await AuthenticationService.RequestAccessToken(
164-
new AccessTokenRequestOptions
165-
{
166-
Scopes = new[] { "https://graph.microsoft.com/Mail.Send",
167-
"https://graph.microsoft.com/User.Read" }
168-
});
169-
```
170-
171197
## Customize app routes
172198

173199
By default, the `Microsoft.AspNetCore.Components.WebAssembly.Authentication` library uses the routes shown in the following table for representing different authentication states.

aspnetcore/security/blazor/webassembly/hosted-with-azure-active-directory-b2c.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description:
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: riande
77
ms.custom: mvc
8-
ms.date: 03/22/2020
8+
ms.date: 03/30/2020
99
no-loc: [Blazor, SignalR]
1010
uid: security/blazor/webassembly/hosted-with-azure-active-directory-b2c
1111
---
@@ -263,9 +263,13 @@ builder.Services.AddMsalAuthentication(options =>
263263

264264
Run the app from the Server project. When using Visual Studio, select the Server project in **Solution Explorer** and select the **Run** button in the toolbar or start the app from the **Debug** menu.
265265

266+
[!INCLUDE[](~/includes/blazor-security/usermanager-signinmanager.md)]
267+
266268
[!INCLUDE[](~/includes/blazor-security/troubleshoot.md)]
267269

268270
## Additional resources
269271

272+
* [Request additional access tokens](xref:security/blazor/webassembly/additional-scenarios#request-additional-access-tokens)
270273
* <xref:security/authentication/azure-ad-b2c>
271274
* [Tutorial: Create an Azure Active Directory B2C tenant](/azure/active-directory-b2c/tutorial-create-tenant)
275+
* [Microsoft identity platform documentation](/azure/active-directory/develop/)

0 commit comments

Comments
 (0)