Skip to content

Commit 5658e86

Browse files
authored
Name and role for Blazor with Identity Server (#18278)
1 parent 2ac9a9a commit 5658e86

File tree

1 file changed

+189
-1
lines changed

1 file changed

+189
-1
lines changed

aspnetcore/security/blazor/webassembly/hosted-with-identity-server.md

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: To create a new Blazor hosted app with authentication from within V
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: riande
77
ms.custom: mvc
8-
ms.date: 05/11/2020
8+
ms.date: 05/19/2020
99
no-loc: [Blazor, "Identity", "Let's Encrypt", Razor, SignalR]
1010
uid: security/blazor/webassembly/hosted-with-identity-server
1111
---
@@ -234,6 +234,194 @@ Run the app from the Server project. When using Visual Studio, either:
234234
* Set the **Startup Projects** drop down list in the toolbar to the *Server API app* and select the **Run** button.
235235
* Select the Server project in **Solution Explorer** and select the **Run** button in the toolbar or start the app from the **Debug** menu.
236236

237+
## Name and role claim with API authorization
238+
239+
Identity Server can be configured to send `name` and `role` claims for authenticated users.
240+
241+
In the Client app, create a custom user factory. Identity Server sends multiple roles as a JSON array in a single `role` claim. A single role is sent as a string value in the claim. The factory creates an individual `role` claim for each of the user's roles.
242+
243+
*CustomUserFactory.cs*:
244+
245+
```csharp
246+
using System.Linq;
247+
using System.Security.Claims;
248+
using System.Text.Json;
249+
using System.Threading.Tasks;
250+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
251+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
252+
253+
public class CustomUserFactory
254+
: AccountClaimsPrincipalFactory<RemoteUserAccount>
255+
{
256+
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
257+
: base(accessor)
258+
{
259+
}
260+
261+
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
262+
RemoteUserAccount account,
263+
RemoteAuthenticationUserOptions options)
264+
{
265+
var user = await base.CreateUserAsync(account, options);
266+
267+
if (user.Identity.IsAuthenticated)
268+
{
269+
var identity = (ClaimsIdentity)user.Identity;
270+
var roleClaims = identity.FindAll(identity.RoleClaimType);
271+
272+
if (roleClaims != null && roleClaims.Any())
273+
{
274+
foreach (var existingClaim in roleClaims)
275+
{
276+
identity.RemoveClaim(existingClaim);
277+
}
278+
279+
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
280+
281+
if (rolesElem is JsonElement roles)
282+
{
283+
if (roles.ValueKind == JsonValueKind.Array)
284+
{
285+
foreach (var role in roles.EnumerateArray())
286+
{
287+
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
288+
}
289+
}
290+
else
291+
{
292+
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
293+
}
294+
}
295+
}
296+
}
297+
298+
return user;
299+
}
300+
}
301+
```
302+
303+
In the Client app, register the factory in `Program.Main` (*Program.cs*):
304+
305+
```csharp
306+
builder.Services.AddApiAuthorization()
307+
.AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
308+
```
309+
310+
* In the Server app, call <xref:Microsoft.AspNetCore.Identity.IdentityBuilder.AddRoles*> on the Identity builder, which adds role-related services:
311+
312+
```csharp
313+
using Microsoft.AspNetCore.Identity;
314+
315+
...
316+
317+
services.AddDefaultIdentity<ApplicationUser>(options =>
318+
options.SignIn.RequireConfirmedAccount = true)
319+
.AddRoles<IdentityRole>()
320+
.AddEntityFrameworkStores<ApplicationDbContext>();
321+
```
322+
323+
* In the Server app:
324+
325+
* Configure Identity Server to put the `name` and `role` claims into the ID token and access token.
326+
* Prevent the default mapping for roles in the JWT token handler.
327+
328+
```csharp
329+
using System.IdentityModel.Tokens.Jwt;
330+
using System.Linq;
331+
332+
...
333+
334+
services.AddIdentityServer()
335+
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
336+
options.IdentityResources["openid"].UserClaims.Add("name");
337+
options.ApiResources.Single().UserClaims.Add("name");
338+
options.IdentityResources["openid"].UserClaims.Add("role");
339+
options.ApiResources.Single().UserClaims.Add("role");
340+
});
341+
342+
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
343+
```
344+
345+
Component authorization approaches are functional at this point. Any of the authorization mechanisms in components can use a role to authorize the user:
346+
347+
* [AuthorizeView component](xref:security/blazor/index#authorizeview-component) (Example: `<AuthorizeView Roles="admin">`)
348+
* [`[Authorize]` attribute directive](xref:security/blazor/index#authorize-attribute) (Example: `@attribute [Authorize(Roles = "admin")]`)
349+
* [Procedural logic](xref:security/blazor/index#procedural-logic) (Example: `if (user.IsInRole("admin")) { ... }`)
350+
351+
Multiple role tests are supported:
352+
353+
```csharp
354+
if (user.IsInRole("admin") && user.IsInRole("developer"))
355+
{
356+
...
357+
}
358+
```
359+
360+
`User.Identity.Name` is populated in the Client app with the user's username, which is usually their sign-in email address.
361+
362+
## Profile Service
363+
364+
In the Server app, create a `ProfileService` implementation. The Profile Service example in this section creates `name` and `role` claims for users similar to the scenario shown in the [Name and role claim with API authorization](#name-and-role-claim-with-api-authorization) section. The value of the `role` claim represents the user's assigned roles.
365+
366+
*ProfileService.cs*:
367+
368+
```csharp
369+
using IdentityModel;
370+
using IdentityServer4.Models;
371+
using IdentityServer4.Services;
372+
using System.Threading.Tasks;
373+
374+
public class ProfileService : IProfileService
375+
{
376+
public ProfileService()
377+
{
378+
}
379+
380+
public Task GetProfileDataAsync(ProfileDataRequestContext context)
381+
{
382+
var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
383+
context.IssuedClaims.AddRange(nameClaim);
384+
385+
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
386+
context.IssuedClaims.AddRange(roleClaims);
387+
388+
return Task.CompletedTask;
389+
}
390+
391+
public Task IsActiveAsync(IsActiveContext context)
392+
{
393+
return Task.CompletedTask;
394+
}
395+
}
396+
```
397+
398+
In the Server app, register the Profile Service in `Startup.ConfigureServices`:
399+
400+
```csharp
401+
using IdentityServer4.Services;
402+
403+
...
404+
405+
services.AddTransient<IProfileService, ProfileService>();
406+
```
407+
408+
Component authorization approaches are functional at this point. Any of the authorization mechanisms in components can a role to authorize the user:
409+
410+
* [AuthorizeView component](xref:security/blazor/index#authorizeview-component) (Example: `<AuthorizeView Roles="admin">`)
411+
* [`[Authorize]` attribute directive](xref:security/blazor/index#authorize-attribute) (Example: `@attribute [Authorize(Roles = "admin")]`)
412+
* [Procedural logic](xref:security/blazor/index#procedural-logic) (Example: `if (user.IsInRole("admin")) { ... }`)
413+
414+
Multiple role tests are supported:
415+
416+
```csharp
417+
if (user.IsInRole("admin") && user.IsInRole("developer"))
418+
{
419+
...
420+
}
421+
```
422+
423+
`User.Identity.Name` is populated in the Client app with the user's user name, which is usually their sign-in email address.
424+
237425
[!INCLUDE[](~/includes/blazor-security/usermanager-signinmanager.md)]
238426

239427
[!INCLUDE[](~/includes/blazor-security/troubleshoot.md)]

0 commit comments

Comments
 (0)