Search code examples
authenticationkubernetesblazorauthorizationscaleout

Blazor Hosted WebAssembly with multiple replicas in kubernetes causing authentication issues


I have an application written in Blazor WebAssembly under .Net8. This app is hosted model which means that has a client and server project. For the intercommunication web api is used. The app is using authentication and authorization.

The Server Program.cs file

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.DataProtectionProvider = DataProtectionProvider.Create(Parameters.app_protector);
        options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
        options.SlidingExpiration = true;
        options.LoginPath = new PathString("/auth/login");
        options.LogoutPath = new PathString("/auth/logout");
        options.AccessDeniedPath = new PathString("/");
        options.Cookie = new CookieBuilder();
        options.Cookie.Name = Parameters.app_cookie;
        options.Cookie.MaxAge = options.ExpireTimeSpan;
        options.Cookie.SameSite = SameSiteMode.Strict; //TESTING WITH STRICT
        options.Cookie.SecurePolicy = CookieSecurePolicy.None;
        options.CookieManager = new ChunkingCookieManager();
        options.EventsType = typeof(CustomCookieAuthenticationEvents);
    });

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

The Server Controller for SignIn:

[HttpPost, Route("/api/auth/login")]
public IActionResult AuthLogin(Authentication authentication)
{
    try
    {
        int auth_id = _IAuth.AuthLogin(authentication); //VALIDATE INFO IN DATABASE
        if (auth_id != -1)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Sid, auth_id.ToString()),
                new Claim(ClaimTypes.Name, authentication.user_name.ToString()),
            };
            var claimsIdentity = new ClaimsIdentity(claims, "Authentication");

            var properties = new AuthenticationProperties()
            {
                IsPersistent = true,
                AllowRefresh = true
            };

            HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), properties);
        }
        return Ok(auth_id); //RETURN A NUMBER GRATER FROM -1 TO SIGN IN
    }
    catch { throw; }
}

The authentication in Client is handled via a CustomAuthStateProvider

HttpResponseMessage httpResponseMessage = await _httpClient.GetAsync("/api/auth/check");
if (httpResponseMessage.IsSuccessStatusCode)
{
    HttpContent content = httpResponseMessage.Content;
    AuthenticationData? authenticationData_ = await content.ReadFromJsonAsync<AuthenticationData?>();
    authenticationData = CheckAuthenticationData(authenticationData_);
    
    //HELPER MESSAGE FOR BROWSER
    if (authenticationData != null)
    { Console.WriteLine("user_is_authorized"); }
    else
    { Console.WriteLine("user_not_authorized"); }
}

Which gets a response from the Server Controller:

AuthenticationData? authenticationData = new AuthenticationData();
var authorizationResult = _authorizationService.AuthorizeAsync(User, UserPolicy).Result;
if (authorizationResult.Succeeded)
{
    authenticationData.Sid = User.Claims.Where(x => x.Type == ClaimTypes.Sid).Select(x => x.Value).FirstOrDefault().NStringToString();
    authenticationData.Name = User.Claims.Where(x => x.Type == ClaimTypes.Name).Select(x => x.Value).FirstOrDefault().NStringToString();

    HttpContext.AuthenticateAsync();
}
return authenticationData;

Everything works fine when work locally, or an AzureWebApp or into Kubernetes Cluster, but as soon as we scale out with replicas more than 1, failures starting appear (in this case signalr methods I use for some messages are failing, but also authentication): ![enter image description here

I have done some investigation and I think that the server holds some state of the authenticated user, as refreshing the page constantly one(1) of the three(3) times works, and considering that replicas are set to 3, it makes sense to me as kubernetes might have Round-Robin into loadbalancher as default method for distributing calls. I am also receiving the message "user_not_authorized" from the console write: enter image description here

If am right, why server keeps any kind of state? Or any other idea which might I missed??


Solution

  • I found my issue in this particular case when scaling out (using pod replicas into Kubernetes cluster), after I identified (with the help of your comments) that the Server is also validating the cookie from the Client.

    In the Server project, into Program.cs there was also the code of storing the keys into a safe place (in my case, in the app db), which I was believing it was used properly, and indeed the key was existing into db:

    builder.Services.AddOptions<KeyManagementOptions>()
        .Configure<IServiceScopeFactory>((options, factory) =>
        {
            options.XmlRepository = new CustomXmlRepository(factory);
        });
    

    But the DataProtectionProvider was not using the key stored into db into the cookie because the following line of code needed to be removed, as it was creating another in-memory DataProtectionProvider (probably with different key):

    options.DataProtectionProvider = DataProtectionProvider.Create(Parameters.app_protector);
    

    Leaving the cookie options without specifying the DataProtectionProvider solved my problem. I believe is taking the default one now.