Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>EqDemo.Client</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="6.0.25" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.11" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;

using Microsoft.AspNetCore.Components.Authorization;

namespace EqDemo.Client;

/// <summary>
/// Resolves the current user's authentication state by calling a small endpoint
/// on the server (which authenticates the request via the ASP.NET Core Identity
/// auth cookie). Replaces the OIDC-based remote authentication state provider
/// that came with Microsoft.AspNetCore.ApiAuthorization.IdentityServer.
/// </summary>
public sealed class HostAuthenticationStateProvider : AuthenticationStateProvider
{
private const string AuthenticationType = "Cookies";

private static readonly AuthenticationState Anonymous =
new(new ClaimsPrincipal(new ClaimsIdentity()));

private readonly HttpClient _http;

public HostAuthenticationStateProvider(HttpClient http)
{
_http = http;
}

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var response = await _http.GetAsync("_auth/me");
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
{
return Anonymous;
}

response.EnsureSuccessStatusCode();

var info = await response.Content.ReadFromJsonAsync<AuthInfo>();
if (info is null || !info.IsAuthenticated)
{
return Anonymous;
}

var claims = (info.Claims ?? Array.Empty<AuthClaim>())
.Select(c => new Claim(c.Type, c.Value));
var identity = new ClaimsIdentity(claims, AuthenticationType, ClaimTypes.Name, ClaimTypes.Role);
return new AuthenticationState(new ClaimsPrincipal(identity));
}
catch (HttpRequestException)
{
return Anonymous;
}
}

private sealed record AuthInfo(bool IsAuthenticated, AuthClaim[]? Claims);

private sealed record AuthClaim(string Type, string Value);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />
@inject NavigationManager Navigation

@code{
@code {
[Parameter] public string? Action { get; set; }

protected override void OnInitialized()
{
// The legacy OIDC client-side authentication flow has been replaced with
// server-side cookie auth via ASP.NET Core Identity Razor Pages. Redirect
// legacy /authentication/{action} URLs to the new endpoints.
var target = Action switch
{
"login" => "Identity/Account/Login",
"register" => "Identity/Account/Register",
"logout" => "Identity/Account/Logout",
"profile" => "Identity/Account/Manage",
_ => "/"
};

Navigation.NavigateTo(target, forceLoad: true);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
@page "/reports"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal
@attribute [Authorize]
@implements IAsyncDisposable

@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IAccessTokenProviderAccessor AccessTokenProviderAccessor

<style>
.eqv-chart-panel {
Expand Down Expand Up @@ -122,13 +119,10 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) {
var result = await AccessTokenProviderAccessor.TokenProvider.RequestAccessToken();
if (result.Status == AccessTokenResultStatus.Success && result.TryGetToken(out var token)) {
await JSRuntime.InvokeVoidAsync("easyquery.blazor.startAdhocReporting", token.Value);
}
else {
await JSRuntime.InvokeVoidAsync("consloe.error", "Unable to get access token");
}
// With cookie auth the browser sends the auth cookie automatically with
// every same-origin request, so no bearer token has to be passed to the
// EasyQuery JS bootstrap.
await JSRuntime.InvokeVoidAsync("easyquery.blazor.startAdhocReporting", string.Empty);
}

await base.OnAfterRenderAsync(firstRender);
Expand Down
12 changes: 7 additions & 5 deletions AspNetCore/Blazor/AdHocReporting.BlazorWasm/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

using EqDemo.Client;
Expand All @@ -8,12 +8,14 @@
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddHttpClient("EqDemo.BlazorWasm.AdhocReporting.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddHttpClient("EqDemo.BlazorWasm.AdhocReporting.ServerAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

// Supply HttpClient instances that include access tokens when making requests to the server project
// Supply HttpClient instances that use the same origin as the server (so the
// ASP.NET Core Identity auth cookie is sent automatically with every request).
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("EqDemo.BlazorWasm.AdhocReporting.ServerAPI"));

builder.Services.AddApiAuthorization();
builder.Services.AddAuthorizationCore();
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
builder.Services.AddScoped<AuthenticationStateProvider, HostAuthenticationStateProvider>();

await builder.Build().RunAsync();
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
<Authorized>
<a href="authentication/profile">Hello, @context.User.Identity?.Name!</a>
<button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
<a href="authentication/logout" class="nav-link btn btn-link">Log out</a>
</Authorized>
<NotAuthorized>
<a href="authentication/register">Register</a>
<a href="authentication/login">Log in</a>
</NotAuthorized>
</AuthorizeView>

@code{
private async Task BeginSignOut(MouseEventArgs args)
{
await SignOutManager.SetSignOutState();
Navigation.NavigateTo("authentication/logout");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@code {
protected override void OnInitialized()
{
Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
var returnUrl = new Uri(Navigation.Uri).PathAndQuery;
Navigation.NavigateTo($"Identity/Account/Login?ReturnUrl={Uri.EscapeDataString(returnUrl)}", forceLoad: true);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

using Duende.IdentityServer.EntityFramework.Options;

using EqDemo.Models;

namespace EqDemo.Data
{
public class AppDbContext : ApiAuthorizationDbContext<ApplicationUser>
public class AppDbContext : IdentityDbContext<ApplicationUser>
{
public AppDbContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>EqDemo.BlazorWasm.AdhocReporting.Server</UserSecretsId>
<RootNamespace>EqDemo.Server</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.7" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.34.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.6.0" />
<PackageReference Include="NuGet.Common" Version="5.11.5" />
<PackageReference Include="NuGet.Protocol" Version="5.11.5" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.6" />
<PackageReference Include="System.Drawing.Common" Version="4.7.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.34.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Client\EqDemo.BlazorWasm.AdhocReporting.Client.csproj" />
<ProjectReference Include="..\Shared\EqDemo.BlazorWasm.AdhocReporting.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
</ItemGroup>
<ItemGroup>
<!-- DB initialization packages. They are not necessary for EasyQuery working and can be removed in production -->
Expand All @@ -48,4 +43,4 @@
<PackageReference Include="Korzh.EasyQuery.EntityFrameworkCore.Relational" Version="7.4.0" />
<PackageReference Include="Korzh.EasyQuery.EntityFrameworkCore.Identity" Version="7.4.0" />
</ItemGroup>
</Project>
</Project>
34 changes: 19 additions & 15 deletions AspNetCore/Blazor/AdHocReporting.BlazorWasm/Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -29,18 +27,12 @@
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, AppDbContext>(options => {
//the following 2 lines are necessary to support roles on the WebAssembly side
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});

// We need to do this as it maps "role" to ClaimTypes.Role and causes issues
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

builder.Services.AddAuthentication()
.AddIdentityServerJwt();
// Override the role-blind UserClaimsPrincipalFactory<TUser> that AddDefaultIdentity
// pre-registers so the chained AddRoles<IdentityRole>() actually projects role
// claims into the auth cookie's ClaimsPrincipal. Without this the IUserClaimsPrincipalFactory<>
// registration from AddIdentityCore wins (TryAddScoped) and User.IsInRole(...) is always false.
builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>,
UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();

//EasyQuery services
builder.Services.AddEasyQuery()
Expand Down Expand Up @@ -79,7 +71,6 @@

app.UseRouting();

app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();

Expand Down Expand Up @@ -118,6 +109,19 @@

app.MapRazorPages();
app.MapControllers();

// Lightweight endpoint used by the Blazor WASM client's HostAuthenticationStateProvider
// to project the cookie-authenticated user's claims back to the WebAssembly process.
app.MapGet("/_auth/me", (HttpContext ctx) =>
{
var user = ctx.User;
var isAuthenticated = user?.Identity?.IsAuthenticated == true;
var claims = isAuthenticated
? user!.Claims.Select(c => new { Type = c.Type, Value = c.Value }).ToArray()
: Array.Empty<object>();
return Results.Ok(new { IsAuthenticated = isAuthenticated, Claims = claims });
});

app.MapFallbackToFile("index.html");

//Init demo database (if necessary)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
"Microsoft.AspNetCore": "Warning"
}
},
"IdentityServer": {
"Clients": {
"EqDemo.BlazorWasm.AdhocReporting.Client": {
"Profile": "IdentityServerSPA"
}
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"EqDemoSqLite": "Data Source=eqdemo-sqlite.db",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down