Click here to Skip to main content
15,877,618 members
Please Sign up or sign in to vote.
1.44/5 (2 votes)
Hey everyone,

i don't know if i am doing it entirely wrong but what i am trying to achieve is to Host a Blazor Server App on my IIS with Windows Authentication and an additional customClaim to prevent everyone from accessing, but only a hand full of people.

So far i made my way to the code pasted below, and the thing is, it works with my visual studio while debugging. It's all good there and does the Job as expected, but as soon as i publish it to the IIS Server it restricts my access.

If anyone could lead me the right direction i'd be super happy.

Thanks in advance!

What I have tried:

Code to Authorize the Users (program.cs)

using System.Security.Claims;
using System.Security.Principal;
using ImsServerMonitor.Data;
using Microsoft.AspNetCore.Authentication.Negotiate;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate(options =>
    {
        options.Events = new NegotiateEvents
        {
            OnAuthenticated = context =>
            {
                if (context.Principal.Identity is WindowsIdentity windowsIdentity)
                {
                    string 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")
                        };

                        context.Principal.AddIdentity(new ClaimsIdentity(claims));
                    }
                }

                return Task.CompletedTask;
            }
        };
    });
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policyBuilder => policyBuilder.RequireClaim("CustomClaim", "Admin"));
    // By default, all incoming requests will be authorized according to the default policy.
    //options.FallbackPolicy = options.DefaultPolicy;
    options.FallbackPolicy = options.GetPolicy("AdminOnly");
});

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<MonitorEngine>();
builder.Services.AddDevExpressBlazor();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseStaticFiles();

app.UseRouting();

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

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();


Additionally on the IIS ofc i have WindowsAuth enabled for this site and verified that the Negotiate Provider is on top of the list.

But i really don't get what the issue is, even ChatGpt failed me... or i failed to ask the right question.
Posted
Updated 13-Jun-23 4:19am
Comments
Richard Deeming 13-Jun-23 8:11am    
I'm assuming that code is just for testing purposes? loginName.Contains("User1") will match User10, User123, DontTrustThisUser13, etc.

Have you debugged your code to ensure that the OnAuthenticated handler is getting hit, and the context.Principal.Identity is actually a WindowsIdentity?
HobbyProggy 13-Jun-23 9:40am    
Hi Richard, yes the User strings are simply for displaying information here, in the original its referring to actual login names that are properly fetched from the Windows Authentication. Just can't display the real ones here.

I did debug and as written above, within debug on my machine all goes smooth as expected, it runs through that routine and verifies if the user is one of the four allowed and adds the specific admin claim. Then the page gets displayed. I removed my user and got the restricted access, so i am assuming it should work.
Richard Deeming 13-Jun-23 9:50am    
Well, you know what "assume" does. :)

If you can't debug the code running on the server, then start by adding some logging to ensure that the method gets called, the identity is a WindowsIdentity, and the identity name is what you are expecting.
HobbyProggy 13-Jun-23 10:14am    
Yup, thanks again, as always, you seem to point me into the right direction.
The assume was of course a dead end. Because i did not debug with IIS on but directly with that Blazor Project thing that just does magical stuff...

I fixed it and i think i know why it didn't work. I made some Tests and now it works...
Seems like you can't just "overwrite" the windows build in stuff that easy, which does make sense i guess.
HobbyProggy 13-Jun-23 9:46am    
Additionally, i think the identity should also work properly on the iis without the additional negotiate options, since if i remove that part and go back to default policy it works, but then ofc. everyone can see the admin site.

What actually needed to be done... after a lucky research and the check your debug thrice:

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
    {
        
    }
 
Share this answer
 
Comments
Richard Deeming 14-Jun-23 3:29am    
Depending on your C# language version, you can simplify:
!(Context.User.Identity is WindowsIdentity windowsIdentity)

to:
Context.User.Identity is not WindowsIdentity windowsIdentity


You could go even further and replace:
if (!Context.User.Identity.IsAuthenticated || Context.User.Identity is not WindowsIdentity windowsIdentity)

with:
if (Context.User.Identity is not WindowsIdentity { IsAuthenticated: true } windowsIdentity)
HobbyProggy 20-Jun-23 4:38am    
Oh, that's a very good point! Thanks on that :)
Thank you for this question and solution. Works great. I am on .NET 8 and had to make 1 small change in the below code from

var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
to

Context.User.AddIdentity(identity);
var ticket = new AuthenticationTicket(Context.User, Scheme.Name);
return AuthenticateResult.Success(ticket);
Else you cannot access the username elsewhere in code.
In the below code _authMessage returns null if we use the orignal code.

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

    protected override async Task OnInitializedAsync()
    {
        if (AuthenticationState is not null)
        {
            var authState = await AuthenticationState;
            var user = authState.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                _authMessage = user.Identity.Name;
            }
        }
    }
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900