Search code examples
c#.netblazor.net-6.0blazor-server-side

.NET Authentication Cookie shared between clients


I am working on a Blazor server project that uses a separate .NET backend api to handle authentication as well as querying the database. Both instances are using .NET 6. I was under the impression that I had it working ok enough until my boss tried to use the azure instance, signed in and got my session returned somehow instead of theirs.

My working theory is that the HttpContext is behaving a little more like a singleton for my use case than the expected scoped behavior. I have been unable to reproduce the issue for the life of me - it doesn't happen locally and even with multiple clients connected to the azure Blazor server instance I only get the expected behavior of signing in, setting a cookie and using this cookie for the remaining requests in the session. I only have a couple months experience in .NET so I am really flailing about.

The login process begins with a username and password. These are sent to this backend api for validation. If they're valid success is returned along with a user id. This user id is then sent back to the server where a session is created like so:

var claims = new List<Claim>
    {
    
        new Claim("sessionid", sessionId)
    };
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

var sessionResponse = await SessionService.AddSession(new Session { SessionId = sessionId, UserAccessLevel = user.UserTypeInt, UserId = user.RowKey });
if (sessionResponse?.ResponseCode == ResponseCode.Success)
{
        // Set the cookie here
    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, new AuthenticationProperties() {  IsPersistent = true});
    var newSession = sessionResponse?.ResponseObject as Session;
    var loginCombo = new LoginCombo { user = user, session = newSession };
    return Response(ResponseCode.Success, Newtonsoft.Json.JsonConvert.SerializeObject(loginCombo)); // SUCCESS
}

Subsequent requests are expected to have access to this cookie when these requests are from the same Blazor server scope (or whatever you call it when the browser tab continues making requests).

Instead of using authorization/roles we are using a custom method that accesses this cookie with this: bool success = HttpContext.Request.Headers.TryGetValue("sessionid", out var val);.

This works fine and is also the only way I access user information from there on out.

My boss signed in with their username and password, and the following requests must have had the sessionid of another account because the account information returned wasn't associated with their account.

Here's the Program.cs file for my API:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

string sqlConnectionString = builder.Configuration.GetConnectionString("SQLConnectionString");

builder.Services.AddDbContext<CookieAuthContext>(options =>
{
    options.UseSqlServer(sqlConnectionString);
});

KnowServerLibrary.Startup.InitializeExternal(builder.Configuration);

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => {options.LoginPath = "/api/Proprietary/NewLogin"; options.Cookie.SameSite = SameSiteMode.None; options.Cookie.SecurePolicy = CookieSecurePolicy.Always;});

string proprietaryOriginsAllowed = "_proprietaryOriginsAllowed";

builder.Services.AddCors(options =>
{

    options.AddPolicy(name: proprietaryOriginsAllowed, pb =>
    {
        pb.AllowAnyHeader()
                        .AllowAnyMethod()
                        .SetIsOriginAllowed(options => true)
                        .WithOrigins("https://proprietaryAgain.azurewebsites.net", "https://localhost:44397", "https://localhost:44396", "https://localhost:7282", "https://proprietary.cadev.local:4447")
                        .AllowCredentials();
    });

});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseCors(proprietaryOriginsAllowed);
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
await ProprietaryTableManager.InitializeDatabaseSchemaUpdates();
app.Run();

And the Program.cs from my Blazor server (ProprietaryAPI being the backend API):

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Http;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<CustomAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CustomAuthStateProvider>());
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => false;
    options.MinimumSameSitePolicy = SameSiteMode.None;
});

builder.Services.AddHttpClient(Microsoft.Extensions.Options.Options.DefaultName, options =>
{
    options.BaseAddress = new Uri(ProprietaryAPI.baseURI);
}).SetHandlerLifetime(Timeout.InfiniteTimeSpan);

builder.Services.AddScoped<HttpClient>();
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddScoped<IProprietaryAPI, ProprietaryAPI>();
builder.Logging.AddDebug();
builder.Services.AddBlazorBootstrap();
builder.Services.AddWMBSC(false);
var app = builder.Build();
app.UseRouting();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseStaticFiles();



app.UseAuthentication();

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    app.MapBlazorHub();
    app.MapFallbackToPage("/_Host");
});

app.Run();

The AuthService and CustomAuthStateProvider don't really do much (Service sets session in local storage, CustomAuthStateProvider calls the service).


Solution

  • In case anyone else ever encounters this:

    The answer was convoluted and not very well documented. The suggested solution was for JWT but works for cookies as well. Additionally, more work is needed to access any sort of Token Provider that shares scope with the blazor application and my http client - an http client factory subclass fixes this.