Reputation: 11
I have a solution in .NET 7 that works ok, however I want to migrate to .NET 8 with new structure that MS presented and I am facing a lot of issues as authentication does not work the way I would like to.
So in AuthService I have a logic to get data from AD. I get there First name, Last Name, Email, Department based on the username I get in CustomServerAuthenticationStateProvider.GetAuthenticationStateAsync. With this approach I get SSO.
If user has not been on site yet I register him and create a record in database where I am later on able to assign him a proper roles that he needs for his work. If his username is already found in database then I just read his roles and assing them to ClaimsIdentity -> userWinIdentity.AddClaims(_userService.GetRolesClaims());
So I migrated everyting to new .NET8 solution and it works if I come to the page with clicking to link from main menu that navigates user to Profile.razor page. When I am there and press F5 to refresh the page then I get this error:
Access to host localhost is denied.
You do not have user rights to view this page.
HTTP 403 ERROR
Program.cs
using BlazorServer.Data;
using BlazorServer.Providers;
using BlazorServer.Services;
using BlazorServer.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using BlazorServer.Consts;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddControllers();
builder.Services.AddLocalization();
builder.Services.AddMudServices();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<RoleService>();
builder.Services.AddScoped<TimeZoneService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomServerAuthenticationStateProvider>();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseHttpsRedirection();
app.UseRouting();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapControllers();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(CultureConsts.supportedCultures[0])
.AddSupportedCultures(CultureConsts.supportedCultures)
.AddSupportedUICultures(CultureConsts.supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.Run();
CustomServerAuthenticationStateProvider.cs
public class CustomServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
{
private readonly AuthService _authService;
private readonly UserService _userService;
private readonly IHttpContextAccessor _httpContextAccessor;
private Task<AuthenticationState> _authenticationStateTask;
public CustomServerAuthenticationStateProvider(AuthService authService, UserService userService, IHttpContextAccessor httpContextAccessor)
{
_authService = authService;
_userService = userService;
_httpContextAccessor = httpContextAccessor;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
var userWinIdentity = (ClaimsIdentity)httpContext?.User.Identity!;
if (userWinIdentity != null && userWinIdentity.IsAuthenticated && !string.IsNullOrEmpty(userWinIdentity.Name))
{
var username = userWinIdentity.Name.Split('\\').Last();
User userObj = _authService.Authenticate(username);
_userService.User = userObj;
userWinIdentity.AddClaims(_userService.AddCustomUserClaims());
userWinIdentity.AddClaims(_userService.GetRolesClaims());
}
var user = new ClaimsPrincipal(userWinIdentity);
return await Task.FromResult(new AuthenticationState(user));
}
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
{
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
NotifyAuthenticationStateChanged(_authenticationStateTask);
}
}
UserService.cs
public List<Claim> AddCustomUserClaims()
{
return
[
new Claim("FullName", User.FullName)
];
}
public List<Claim> GetRolesClaims()
{
List<Claim> claims = new();
foreach (var role in User.Roles)
claims.Add(new Claim(ClaimTypes.GroupSid, role.Name));
// if I use ClaimTypes.Role then it does not work properly on Profile.razor, I do not know why. If someone has a clue please let me know
return claims;
}
Profile.razor
@page "/profile"
@attribute [Authorize(Roles = $"{SecurityGroup.RegisteredUser}")]
@inject UserService userService
@rendermode RenderMode.InteractiveServer
<PageTitle>@localizer["Info portal"] | @localizer["Profile"]</PageTitle>
<AuthorizeView>
@if (user != null)
{
<div class="container-fluid">
<div class="row">
<div class="col-xs-3 col-sm-3 col-md-3 col-lg-2" style="font-weight: bold;">
@localizer["Username"]
</div>
<div class="col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.Username
</div>
</div>
<div class="row">
<div class="col-xs-3 col-sm-3 col-md-3 col-lg-2" style="font-weight: bold;">
@localizer["First name"]
</div>
<div class="col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.FirstName
</div>
</div>
<div class="row">
<div class="col-xs-3 col-sm-3 col-md-3 col-lg-2" style="font-weight: bold;">
@localizer["Last name"]
</div>
<div class="col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.LastName
</div>
</div>
<div class="row">
<div class="col-xs-3 col-sm-3 col-md-3 col-lg-2" style="font-weight: bold;">
@localizer["Email"]
</div>
<div class="col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.Email
</div>
</div>
<div class="row">
<div class="col-xs-3 col-sm-3 col-md-3 col-lg-2" style="font-weight: bold;">
@localizer["Department"]
</div>
<div class="col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.Department
</div>
</div>
</div>
}
</AuthorizeView>
@code {
User user = new();
protected override async Task OnInitializedAsync()
{
user = userService.User;
}
}
User.cs
public class User
{
public User()
{
Roles = new HashSet<Role>();
}
[Key]
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PersonalNr { get; set; } = string.Empty;
public virtual ICollection<Role> Roles { get; set; }
}
I set following properties to be able to get user that is logged in windows acount: Double click on project:
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
**<UseWindowsService>true</UseWindowsService>**
Right click on project -> properties -> debug -> open debug launch profiles -> Enable Windows Authentication
The issue is that CustomServerAuthenticationStateProvider is not triggered on page refresh. I was checking already everywhere and checking a lot of different approaches but none of them work, so I can't figure out what I need to do to solve that.
There is also one mistery that I do not know the answer. In UserService I have a method to add claims based on roles that user has defined in database. If I add them like that: claims.Add(new Claim(ClaimTypes.GroupSid, role.Name)); then authorization works in production.
If I fake a login on my development machine like that:
userWinIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "username"),
}, "Fake authentication type");
then I need to update the code in UserService and use it like that claims.Add(new Claim(ClaimTypes.Role, role.Name));
This is like that in .NET 7. If anyone knows why it behaves differently in development and production please let me know.
So any help would be greatly appreciated.
EDIT: I was playing arround and I figure out that from what I get from template I changed Routes.razor, basically I added even though it should not be needed. If I remove that piece of code then my code does not even work.
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
In that case I get that error:
An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an object.
InfoPortal.Components.Layout.UserButton.<BuildRenderTree>b__0_5(RenderTreeBuilder __builder2)
Stack Query Cookies Headers Routing
NullReferenceException: Object reference not set to an instance of an object.
InfoPortal.Components.Layout.UserButton.<BuildRenderTree>b__0_5(RenderTreeBuilder __builder2)
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, RenderFragment fragment)
Microsoft.AspNetCore.Components.Authorization.AuthorizeViewCore.BuildRenderTree(RenderTreeBuilder builder)
Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception renderFragmentException)
Microsoft.AspNetCore.Components.RenderTree.Renderer.HandleExceptionViaErrorBoundary(Exception error, ComponentState errorSourceOrNull)
Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()
Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(int componentId, RenderFragment renderFragment)
Microsoft.AspNetCore.Components.RenderHandle.Render(RenderFragment renderFragment)
Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()
Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
Microsoft.AspNetCore.Components.RenderTree.Renderer.HandleExceptionViaErrorBoundary(Exception error, ComponentState errorSourceOrNull)
Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToPendingTasksWithErrorHandling(Task task, ComponentState owningComponentState)
Microsoft.AspNetCore.Components.Rendering.ComponentState.SupplyCombinedParameters(ParameterView directAndCascadingParameters)
Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(ParameterView parameters)
Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(int componentId, ParameterView initialParameters)
Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(IComponent component, ParameterView initialParameters)
Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.RenderEndpointComponent(HttpContext httpContext, Type rootComponentType, ParameterView parameters, bool waitForQuiescence)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext+<>c+<<InvokeAsync>b__10_0>d.MoveNext()
Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
EDIT 2: Sample project https://easyupload.io/5oejk7
Upvotes: 0
Views: 667
Reputation: 11
I managed to solve the issue with one of the solution adviced on that link. https://www.codeproject.com/Questions/5362704/Blazor-windows-authentication-with-custom-claims
The solution that I used is:
program.cs needs to point to a custom authenticator with a custom scheme->
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "CustomWindowsAuthentication";
}).AddScheme<CustomWindowsAuthenticationOptions, CustomWindowsAuthenticationHandler>("CustomWindowsAuthentication", null);
Then you do indeed need an additional class that handles that stuff:
public class CustomWindowsAuthenticationHandler : AuthenticationHandler<CustomWindowsAuthenticationOptions>
{
public CustomWindowsAuthenticationHandler(
IOptionsMonitor<CustomWindowsAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.User.Identity.IsAuthenticated || !(Context.User.Identity is WindowsIdentity windowsIdentity))
{
return AuthenticateResult.NoResult();
}
var loginName = windowsIdentity.Name;
if (loginName.Contains("User1")
|| loginName.Contains("User2")
|| loginName.Contains("User3")
|| loginName.Contains("User4"))
{
var claims = new List<Claim>
{
new Claim("CustomClaim", "Admin")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
return AuthenticateResult.Fail("Custom authentication failed.");
}
}
And for the sake of completeness the "CustomWindowsAuthenticationOptions" because you need that too, although it's empty since i don't need any super special options.
public class CustomWindowsAuthenticationOptions : AuthenticationSchemeOptions
{
}
Upvotes: 1