From d06b81e74a1ce5471aa67e25bcbb1b0c5d33d8c8 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 13 Dec 2023 06:26:53 -0500 Subject: [PATCH 1/2] Add roles to WASM+Identity sample app --- .../Backend/Program.cs | 40 +++++++++++++- .../Backend/SeedData.cs | 53 +++++++++++++++++++ .../Components/Layout/NavMenu.razor | 10 ++++ .../Components/Pages/PrivateEditorPage.razor | 41 ++++++++++++++ .../Components/Pages/PrivateManagerPage.razor | 41 ++++++++++++++ .../CookieAuthenticationStateProvider.cs | 43 +++++++++++++-- 6 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs create mode 100644 8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateEditorPage.razor create mode 100644 8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateManagerPage.razor diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs index 89672cdf0..36e89b200 100644 --- a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs @@ -1,7 +1,9 @@ +using System.Data; using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Backend; var builder = WebApplication.CreateBuilder(args); @@ -16,6 +18,7 @@ // add identity and opt-in to endpoints builder.Services.AddIdentityCore() + .AddRoles() .AddEntityFrameworkStores() .AddApiEndpoints(); @@ -38,6 +41,15 @@ var app = builder.Build(); +#if DEBUG +using (var scope = app.Services.CreateScope()) +{ + var services = scope.ServiceProvider; + + SeedData.Initialize(services); +} +#endif + // create routes for the identity endpoints app.MapIdentityApi(); @@ -61,10 +73,36 @@ } app.UseHttpsRedirection(); + +app.MapGet("/roles", (ClaimsPrincipal user) => +{ + if (user.Identity is not null && user.Identity.IsAuthenticated) + { + var identity = (ClaimsIdentity)user.Identity; + var roles = identity.FindAll(identity.RoleClaimType) + .Select(c => + new + { + c.Issuer, + c.OriginalIssuer, + c.Type, + c.Value, + c.ValueType + }); + + return TypedResults.Json(roles); + } + + return Results.Unauthorized(); +}); + app.Run(); // identity user -class AppUser : IdentityUser { } +class AppUser : IdentityUser +{ + public IEnumerable? Roles { get; set; } +} // identity database class AppDbContext(DbContextOptions options) : IdentityDbContext(options) diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs new file mode 100644 index 000000000..0779a76aa --- /dev/null +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Backend; + +public class SeedData +{ + public static async void Initialize(IServiceProvider serviceProvider) + { + using var context = new AppDbContext(serviceProvider.GetRequiredService>()); + + if (context.Users.Any()) + { + return; + } + + string[] roles = [ "Administrator", "Manager" ]; + using var roleManager = serviceProvider.GetRequiredService>(); + + foreach (var role in roles) + { + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + + } + } + + using var userManager = serviceProvider.GetRequiredService>(); + + var user = new AppUser + { + Email = "bob@contoso.com", + NormalizedEmail = "BOB@CONTOSO.COM", + UserName = "bob@contoso.com", + NormalizedUserName = "BOB@CONTOSO.COM", + EmailConfirmed = true, + SecurityStamp = Guid.NewGuid().ToString("D") + }; + + var password = new PasswordHasher(); + var hashed = password.HashPassword(user, "Passw0rd!"); + user.PasswordHash = hashed; + + await userManager.AddToRolesAsync(user, roles); + + var userStore = new UserStore(context); + var result = userStore.CreateAsync(user); + + await context.SaveChangesAsync(); + } +} diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Layout/NavMenu.razor b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Layout/NavMenu.razor index 03a1ed095..c8b0d45a6 100644 --- a/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Layout/NavMenu.razor +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Layout/NavMenu.razor @@ -31,6 +31,16 @@ Private Page + + diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateEditorPage.razor b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateEditorPage.razor new file mode 100644 index 000000000..172ea4134 --- /dev/null +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateEditorPage.razor @@ -0,0 +1,41 @@ +@page "/private-editor-page" +@attribute [Authorize(Roles = "Editor")] +@using System.Security.Claims + +Private Editor Page + +

Private Editor Page

+ + +

Hello, @context.User.Identity?.Name! You're authenticated and you have an Editor role claim, so you can see this page.

+
+ +

Claims

+ +@if (claims.Count() > 0) +{ +
    + @foreach (var claim in claims) + { +
  • @claim.Type: @claim.Value
  • + } +
+} + +@code { + private IEnumerable claims = Enumerable.Empty(); + + [CascadingParameter] + private Task? AuthState { get; set; } + + protected override async Task OnInitializedAsync() + { + if (AuthState == null) + { + return; + } + + var authState = await AuthState; + claims = authState.User.Claims; + } +} diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateManagerPage.razor b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateManagerPage.razor new file mode 100644 index 000000000..cf92d03a8 --- /dev/null +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivateManagerPage.razor @@ -0,0 +1,41 @@ +@page "/private-manager-page" +@attribute [Authorize(Roles = "Manager")] +@using System.Security.Claims + +Private Manager Page + +

Private Manager Page

+ + +

Hello, @context.User.Identity?.Name! You're authenticated and you have a Manager role claim, so you can see this page.

+
+ +

Claims

+ +@if (claims.Count() > 0) +{ +
    + @foreach (var claim in claims) + { +
  • @claim.Type: @claim.Value
  • + } +
+} + +@code { + private IEnumerable claims = Enumerable.Empty(); + + [CascadingParameter] + private Task? AuthState { get; set; } + + protected override async Task OnInitializedAsync() + { + if (AuthState == null) + { + return; + } + + var authState = await AuthState; + claims = authState.User.Claims; + } +} diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs index 62bf20edf..575615c4e 100644 --- a/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Components.Authorization; +using System.Net.Http.Json; using System.Security.Claims; using System.Text.Json; -using System.Net.Http.Json; +using Microsoft.AspNetCore.Components.Authorization; using BlazorWasmAuth.Identity.Models; namespace BlazorWasmAuth.Identity @@ -33,7 +33,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA /// /// Default principal for anonymous (not authenticated) users. /// - private readonly ClaimsPrincipal Unauthenticated = + private readonly ClaimsPrincipal Unauthenticated = new(new ClaimsIdentity()); /// @@ -99,7 +99,7 @@ public async Task RegisterAsync(string email, string password) }; } catch { } - + // unknown error return new FormResult { @@ -134,7 +134,7 @@ public async Task LoginAsync(string email, string password) // success! return new FormResult { Succeeded = true }; - } + } } catch { } @@ -187,6 +187,30 @@ public override async Task GetAuthenticationStateAsync() userInfo.Claims.Where(c => c.Key != ClaimTypes.Name && c.Key != ClaimTypes.Email) .Select(c => new Claim(c.Key, c.Value))); + // tap the roles endpoint for the user's roles + var rolesResponse = await _httpClient.GetAsync("roles"); + + // throw if request fails + rolesResponse.EnsureSuccessStatusCode(); + + // read the response into a string + var rolesJson = await rolesResponse.Content.ReadAsStringAsync(); + + // deserialize the roles string into an array + var roles = JsonSerializer.Deserialize(rolesJson, jsonSerializerOptions); + + // if there are roles, add them to the claims collection + if (roles?.Length > 0) + { + foreach (var role in roles) + { + if (!string.IsNullOrEmpty(role.Type) && !string.IsNullOrEmpty(role.Value)) + { + claims.Add(new Claim(role.Type, role.Value, role.ValueType, role.Issuer, role.OriginalIssuer)); + } + } + } + // set the principal var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider)); user = new ClaimsPrincipal(id); @@ -210,5 +234,14 @@ public async Task CheckAuthenticatedAsync() await GetAuthenticationStateAsync(); return _authenticated; } + + public class RoleClaim + { + public string? Issuer { get; set; } + public string? OriginalIssuer { get; set; } + public string? Type { get; set; } + public string? Value { get; set; } + public string? ValueType { get; set; } + } } } From 81201aaf386e12404da0eaa320574e0d8e35b164 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 15 Feb 2024 08:29:30 -0500 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Stephen Halter --- .../Backend/Program.cs | 9 +++------ .../Backend/SeedData.cs | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs index 36e89b200..59a4a6992 100644 --- a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs @@ -41,14 +41,11 @@ var app = builder.Build(); -#if DEBUG -using (var scope = app.Services.CreateScope()) +if (builder.Environment.IsDevelopment()) { - var services = scope.ServiceProvider; - - SeedData.Initialize(services); + await using var scope = app.Services.CreateAsyncScope(); + await SeedData.InitializeAsync(scope.ServiceProvider); } -#endif // create routes for the identity endpoints app.MapIdentityApi(); diff --git a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs index 0779a76aa..5fe6595fa 100644 --- a/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs +++ b/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs @@ -6,7 +6,7 @@ namespace Backend; public class SeedData { - public static async void Initialize(IServiceProvider serviceProvider) + public static async Task InitializeAsync(IServiceProvider serviceProvider) { using var context = new AppDbContext(serviceProvider.GetRequiredService>()); @@ -23,7 +23,6 @@ public static async void Initialize(IServiceProvider serviceProvider) if (!await roleManager.RoleExistsAsync(role)) { await roleManager.CreateAsync(new IdentityRole(role)); - } }