Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add roles to WASM+Identity sample app #140

Merged
merged 2 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion 8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -16,6 +18,7 @@

// add identity and opt-in to endpoints
builder.Services.AddIdentityCore<AppUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddApiEndpoints();

Expand All @@ -38,6 +41,12 @@

var app = builder.Build();

if (builder.Environment.IsDevelopment())
{
await using var scope = app.Services.CreateAsyncScope();
await SeedData.InitializeAsync(scope.ServiceProvider);
}

// create routes for the identity endpoints
app.MapIdentityApi<AppUser>();

Expand All @@ -61,10 +70,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<IdentityRole>? Roles { get; set; }
}

// identity database
class AppDbContext(DbContextOptions<AppDbContext> options) : IdentityDbContext<AppUser>(options)
Expand Down
52 changes: 52 additions & 0 deletions 8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
ο»Ώusing Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Backend;

public class SeedData
{
public static async Task InitializeAsync(IServiceProvider serviceProvider)
{
using var context = new AppDbContext(serviceProvider.GetRequiredService<DbContextOptions<AppDbContext>>());

if (context.Users.Any())
{
return;
}

string[] roles = [ "Administrator", "Manager" ];
using var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new IdentityRole(role));
}
}

using var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();

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<AppUser>();
var hashed = password.HashPassword(user, "Passw0rd!");
user.PasswordHash = hashed;

await userManager.AddToRolesAsync(user, roles);

var userStore = new UserStore<AppUser>(context);
var result = userStore.CreateAsync(user);

await context.SaveChangesAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@
<span class="bi bi-key" aria-hidden="true"></span> Private Page
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="private-manager-page">
<span class="bi bi-key" aria-hidden="true"></span> Private Manager Page
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="private-editor-page">
<span class="bi bi-key" aria-hidden="true"></span> Private Editor Page
</NavLink>
</div>
</Authorized>
</AuthorizeView>
</nav>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
ο»Ώ@page "/private-editor-page"
@attribute [Authorize(Roles = "Editor")]
@using System.Security.Claims

<PageTitle>Private Editor Page</PageTitle>

<h1>Private Editor Page</h1>

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

<h2>Claims</h2>

@if (claims.Count() > 0)
{
<ul>
@foreach (var claim in claims)
{
<li><b>@claim.Type:</b> @claim.Value</li>
}
</ul>
}

@code {
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }

protected override async Task OnInitializedAsync()
{
if (AuthState == null)
{
return;
}

var authState = await AuthState;
claims = authState.User.Claims;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
ο»Ώ@page "/private-manager-page"
@attribute [Authorize(Roles = "Manager")]
@using System.Security.Claims

<PageTitle>Private Manager Page</PageTitle>

<h1>Private Manager Page</h1>

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

<h2>Claims</h2>

@if (claims.Count() > 0)
{
<ul>
@foreach (var claim in claims)
{
<li><b>@claim.Type:</b> @claim.Value</li>
}
</ul>
}

@code {
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }

protected override async Task OnInitializedAsync()
{
if (AuthState == null)
{
return;
}

var authState = await AuthState;
claims = authState.User.Claims;
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -33,7 +33,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
/// <summary>
/// Default principal for anonymous (not authenticated) users.
/// </summary>
private readonly ClaimsPrincipal Unauthenticated =
private readonly ClaimsPrincipal Unauthenticated =
new(new ClaimsIdentity());

/// <summary>
Expand Down Expand Up @@ -99,7 +99,7 @@ public async Task<FormResult> RegisterAsync(string email, string password)
};
}
catch { }

// unknown error
return new FormResult
{
Expand Down Expand Up @@ -134,7 +134,7 @@ public async Task<FormResult> LoginAsync(string email, string password)

// success!
return new FormResult { Succeeded = true };
}
}
}
catch { }

Expand Down Expand Up @@ -187,6 +187,30 @@ public override async Task<AuthenticationState> 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<RoleClaim[]>(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);
Expand All @@ -210,5 +234,14 @@ public async Task<bool> 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; }
}
}
}