Skip to content

Commit 654d371

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

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed

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

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,194 @@ Component authorization approaches are functional at this point. Any of the auth
422422

423423
`User.Identity.Name` is populated in the Client app with the user's user name, which is usually their sign-in email address.
424424

425+
## Name and role claim with API authorization
426+
427+
Identity Server can be configured to send `name` and `role` claims for authenticated users.
428+
429+
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.
430+
431+
*CustomUserFactory.cs*:
432+
433+
```csharp
434+
using System.Linq;
435+
using System.Security.Claims;
436+
using System.Text.Json;
437+
using System.Threading.Tasks;
438+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
439+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
440+
441+
public class CustomUserFactory
442+
: AccountClaimsPrincipalFactory<RemoteUserAccount>
443+
{
444+
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
445+
: base(accessor)
446+
{
447+
}
448+
449+
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
450+
RemoteUserAccount account,
451+
RemoteAuthenticationUserOptions options)
452+
{
453+
var user = await base.CreateUserAsync(account, options);
454+
455+
if (user.Identity.IsAuthenticated)
456+
{
457+
var identity = (ClaimsIdentity)user.Identity;
458+
var roleClaims = identity.FindAll(identity.RoleClaimType);
459+
460+
if (roleClaims != null && roleClaims.Any())
461+
{
462+
foreach (var existingClaim in roleClaims)
463+
{
464+
identity.RemoveClaim(existingClaim);
465+
}
466+
467+
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
468+
469+
if (rolesElem is JsonElement roles)
470+
{
471+
if (roles.ValueKind == JsonValueKind.Array)
472+
{
473+
foreach (var role in roles.EnumerateArray())
474+
{
475+
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
476+
}
477+
}
478+
else
479+
{
480+
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
481+
}
482+
}
483+
}
484+
}
485+
486+
return user;
487+
}
488+
}
489+
```
490+
491+
In the Client app, register the factory in `Program.Main` (*Program.cs*):
492+
493+
```csharp
494+
builder.Services.AddApiAuthorization()
495+
.AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
496+
```
497+
498+
* In the Server app, call <xref:Microsoft.AspNetCore.Identity.IdentityBuilder.AddRoles*> on the Identity builder, which adds role-related services:
499+
500+
```csharp
501+
using Microsoft.AspNetCore.Identity;
502+
503+
...
504+
505+
services.AddDefaultIdentity<ApplicationUser>(options =>
506+
options.SignIn.RequireConfirmedAccount = true)
507+
.AddRoles<IdentityRole>()
508+
.AddEntityFrameworkStores<ApplicationDbContext>();
509+
```
510+
511+
* In the Server app:
512+
513+
* Configure Identity Server to put the `name` and `role` claims into the ID token and access token.
514+
* Prevent the default mapping for roles in the JWT token handler.
515+
516+
```csharp
517+
using System.IdentityModel.Tokens.Jwt;
518+
using System.Linq;
519+
520+
...
521+
522+
services.AddIdentityServer()
523+
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
524+
options.IdentityResources["openid"].UserClaims.Add("name");
525+
options.ApiResources.Single().UserClaims.Add("name");
526+
options.IdentityResources["openid"].UserClaims.Add("role");
527+
options.ApiResources.Single().UserClaims.Add("role");
528+
});
529+
530+
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
531+
```
532+
533+
Component authorization approaches are functional at this point. Any of the authorization mechanisms in components can use a role to authorize the user:
534+
535+
* [AuthorizeView component](xref:security/blazor/index#authorizeview-component) (Example: `<AuthorizeView Roles="admin">`)
536+
* [`[Authorize]` attribute directive](xref:security/blazor/index#authorize-attribute) (Example: `@attribute [Authorize(Roles = "admin")]`)
537+
* [Procedural logic](xref:security/blazor/index#procedural-logic) (Example: `if (user.IsInRole("admin")) { ... }`)
538+
539+
Multiple role tests are supported:
540+
541+
```csharp
542+
if (user.IsInRole("admin") && user.IsInRole("developer"))
543+
{
544+
...
545+
}
546+
```
547+
548+
`User.Identity.Name` is populated in the Client app with the user's username, which is usually their sign-in email address.
549+
550+
## Profile Service
551+
552+
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.
553+
554+
*ProfileService.cs*:
555+
556+
```csharp
557+
using IdentityModel;
558+
using IdentityServer4.Models;
559+
using IdentityServer4.Services;
560+
using System.Threading.Tasks;
561+
562+
public class ProfileService : IProfileService
563+
{
564+
public ProfileService()
565+
{
566+
}
567+
568+
public Task GetProfileDataAsync(ProfileDataRequestContext context)
569+
{
570+
var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
571+
context.IssuedClaims.AddRange(nameClaim);
572+
573+
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
574+
context.IssuedClaims.AddRange(roleClaims);
575+
576+
return Task.CompletedTask;
577+
}
578+
579+
public Task IsActiveAsync(IsActiveContext context)
580+
{
581+
return Task.CompletedTask;
582+
}
583+
}
584+
```
585+
586+
In the Server app, register the Profile Service in `Startup.ConfigureServices`:
587+
588+
```csharp
589+
using IdentityServer4.Services;
590+
591+
...
592+
593+
services.AddTransient<IProfileService, ProfileService>();
594+
```
595+
596+
Component authorization approaches are functional at this point. Any of the authorization mechanisms in components can a role to authorize the user:
597+
598+
* [AuthorizeView component](xref:security/blazor/index#authorizeview-component) (Example: `<AuthorizeView Roles="admin">`)
599+
* [`[Authorize]` attribute directive](xref:security/blazor/index#authorize-attribute) (Example: `@attribute [Authorize(Roles = "admin")]`)
600+
* [Procedural logic](xref:security/blazor/index#procedural-logic) (Example: `if (user.IsInRole("admin")) { ... }`)
601+
602+
Multiple role tests are supported:
603+
604+
```csharp
605+
if (user.IsInRole("admin") && user.IsInRole("developer"))
606+
{
607+
...
608+
}
609+
```
610+
611+
`User.Identity.Name` is populated in the Client app with the user's user name, which is usually their sign-in email address.
612+
425613
[!INCLUDE[](~/includes/blazor-security/usermanager-signinmanager.md)]
426614

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

0 commit comments

Comments
 (0)