0

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??

2 Answers 2

0

I believe this could be caused by wrong ingress definition, since problem started with more replicas. Did you set up sticky sessions? https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/server?view=aspnetcore-8.0#kubernetes

Sign up to request clarification or add additional context in comments.

6 Comments

Yes I have set these properties in order to test, but are sticky sessions required in my case?
I believe so. Had similar error and sticky session did fixed it. If you have more replicas, you need to ensure that client is comunicating with same replica all the time and is not redirected to different one, since those are like separate servers.
Why each client should communicate with the same server if the server does not keep any authentication state? Normally it should authenticate each request right?
Main reason is that WebSockets need to find its way to correct server. Also server does keep keys to recognize auth cookies. If you dont tell it where to store them, other replicas wont see them. I have this handled with redis, so the keys are accessible from each replica: builder.Services.AddDataProtection().PersistKeysToStackExchangeRedis(redis); link
Ahh I see now, each cookie is encrypted with a key by a server and need to be able to be decrypted with the same way. Good point, but I have already implemented this AddDataProtection by keeping the key into a database which is the same for all replicas, but I will take a look on it again, my code is using builder.Services.AddOptions<KeyManagementOptions>() .Configure<IServiceScopeFactory>((options, factory) => { options.XmlRepository = new CustomXmlRepository(factory); });. Regarding websockets, didn't knew about this, thanks
|
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.