Ryan O
Ryan O

Reputation: 21

Blazor .NET 8 Local AD Auth issue with render mode se to InteractiveServer

I am working with a new Blazor app, where we use local AD to authenticate the user. I have seen very little examples on this out there, but I managed to get it to work. The problem that I have run into, is when the render mode is set to InteractiveServer.

When this is set, the page gets rendered once at the server, then again at the client. In the OnInitializedAsync() method, I am checking a user's rights to determine what information to show. On the first fire of this event, the user is obtained correctly and everything is set up correct. However, on the second call, the user information is gone, so the information will always be hidden.

I have tested it using the normal user account template, and it works. I feel like there is an issue where the user information is not getting set on the client, but I am having issues pinpointing what I am doing wrong.

Program.CS

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddServerSideBlazor();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddTransient<IPersonSvc, PersonSvc>();
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate(options =>
    {
        options.Events = new NegotiateEvents
        {
            OnAuthenticated = context =>
            {
                // Example: Retrieve WindowsIdentity and AD groups
                var windowsIdentity = context.Principal.Identity as WindowsIdentity;
                var adGroups = windowsIdentity?.Groups.Translate(typeof(NTAccount));

                // Add role claims to the user's identity based on AD groups
                if (adGroups != null)
                {
                    foreach (var group in adGroups)
                    {                       
                        context.Principal.Identities.FirstOrDefault().AddClaim(new Claim(ClaimTypes.Role, group.Value));
                    }
                }

                return Task.CompletedTask;
            },
            // You can handle other events as needed
        };
    }).AddIdentityCookies();

builder.Services.Configure<AuthenticationOptions>(options =>
{
    options.DefaultScheme = NegotiateDefaults.AuthenticationScheme;
});
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("No connection string in config.");

builder.Services.AddDbContextFactory<ApplicationDbContext>((DbContextOptionsBuilder options) => 
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorComponents(options => options.DetailedErrors = true);
builder.Services.AddTelerikBlazor();
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    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.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

The page I am working with


@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IPersonSvc PersonService
@attribute [Authorize]
@rendermode InteractiveServer
@page "/PhoneDirectory"

Page Content here
@code {

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

    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    List<Person>? GridData;

    private bool ViewAdditionalInfo { get; set; }
    private bool isInitialized = false;
    private ClaimsPrincipal user;
    
   protected override async Task OnInitializedAsync()
   {
       if (!isInitialized)
       {
           var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
           user = authenticationState.User;
           //First time through I have the user info, second time through it is not authenticated and the isInitialized is false again.

           // Initialization logic here
           await LoadData();
           isInitialized = true;
       }
      

       await base.OnInitializedAsync();
   }
}

App.Razor

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
    <link href="bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <link href="/bootstrap-icons/font/bootstrap-icons.min.css" rel="stylesheet" />
    <link rel="stylesheet" href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" />
    <link rel="stylesheet" href="css/app.css" />
    <link rel="stylesheet" href="InternalSite.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    @* <HeadOutlet @rendermode="RenderMode.InteractiveServer" /> *@
    <HeadOutlet />
</head>
body>
    @* <Routes @rendermode="RenderMode.InteractiveServer" /> *@
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

I have tried both setting the render mode to interactive server here and leaving it and setting it at the page.

I have tried various things with no solution. Any help would be much appreciated.

Upvotes: 1

Views: 604

Answers (1)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30177

You App sets interactivity at the component/page level.

    <Routes />

The SPA Render Tree root component is the page component if it has render mode set. Routes and MainLayout are statically rendered, there's no Cascading Task<AuthenticationState> from Routes in the SPA Render Tree for components to consume.

Change App to this, and you will be running in full Server mode with Routes set as the SPA Render Tree root. The cascading values in Routes are now available to all components in the Render Tree.

    <HeadOutlet @rendermode="RenderMode.InteractiveServer" />

    <Routes @rendermode="RenderMode.InteractiveServer" />

Upvotes: 0

Related Questions