g
We are have umbraco (v13.5.1) in loadbalanced environemnt. We encountered problems with user authentication. We run Umbraco on 3 pods in AWS kubernetes. While refreshing the page after login, we can see that user is randomly signed in and not. This clearly shows that load balancer is sending requst to random pods. We can't use sticky sessions on load balancer (nor cookie nor ip_hash), but would instead like to emply Sessions for that. However we are having trouble setting that. We have SQL cache set up, added the code to startup that is advised by .net and umbraco docs, but it seems to not work. Is there any example what needs to be done for user sessions to be shared between servers? Our Cache composer:
Copy code
public class DistributedCacheComposer : IComposer
    {
        public void Compose(IUmbracoBuilder builder)
        {
            if (ServerInfo.Role == ServerRole.SchedulingPublisher)
            {
                builder.AddNotificationHandler<UmbracoApplicationStartingNotification, RunAddCacheTableMigration>();
            }

            builder.Services.AddDistributedSqlServerCache(options =>
            {
                options.SchemaName = "dbo";
                options.TableName = nameof(CacheStore);
                options.ConnectionString = builder.Config.GetConnectionString("umbracoDbDSN");
            });
        }
    }
Startup:
Copy code
public void ConfigureServices(IServiceCollection services)
{
 services.AddDistributedMemoryCache();
 services.AddSession(options =>
 {
     options.IdleTimeout = TimeSpan.FromMinutes(30);
     options.Cookie.HttpOnly = true;
     options.Cookie.IsEssential = true;
     options.Cookie.Name = "custom_session_cookie";
 });
....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSession();
    .....
}
Is ther something special we need to do on user login action, or do we need a special middleware? Help would really be apriciated ❤️
d
I don’t see anything in your code indicating that the user is authenticated. What are you using for authentication? You would likely need to store their authentication token and id token in session, though I am not sure that would work.
g
I tired to add an entry when the user successfully logged in
Copy code
var result = await signInManager.PasswordSignInAsync(
                user?.UserName ?? model.Username, model.Password, model.RememberMe, true);

if (result.Succeeded)
{
    HttpContext.Session.SetString("UserId", user.Id.ToString());
    return Ok(ModelState);
}
But the table in the SQL remained empty, even though I got the cookie I set for session in the browser.
d
So it sounds like the Sql cache isn’t working at all, let alone for persisting their authentication status across servers.
g
So it seems we figured out the solution for this. Here is the final code changes: In startup we added
Session
and configured
SessionStore
Startup.cs
Copy code
cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddSession();

    services.ConfigureApplicationCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;

        // Use the distributed cache for storing security stamps
        options.SessionStore = new DistributedCacheTicketStore(
            services.BuildServiceProvider().GetRequiredService<IDistributedCache>());
    });
}
We needed to timplement
ITicketStore
interface:
DistributedCacheTicketStore.cs
Copy code
cs
namespace Cms.SessionStore
{
    // Example taken from: https://github.com/bitwarden/server/blob/2e072aebe3a1354965b0018de620703133fec501/src/Core/IdentityServer/DistributedCacheTicketStore.cs

    public class DistributedCacheTicketStore(IDistributedCache cache) : ITicketStore
    {
        private const string KeyPrefix = "auth-";

        public async Task<string> StoreAsync(AuthenticationTicket ticket)
        {
            var key = $"{KeyPrefix}{Guid.NewGuid()}";
            await RenewAsync(key, ticket);
            return key;
        }

        public Task RenewAsync(string key, AuthenticationTicket ticket)
        {
            var options = new DistributedCacheEntryOptions();
            var expiresUtc = ticket.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.AddMinutes(30);
            options.SetAbsoluteExpiration(expiresUtc);
            options.SetSlidingExpiration(TimeSpan.FromMinutes(30));

            var val = SerializeToBytes(ticket);
            cache.Set(key, val, options);
            return Task.FromResult(0);
        }

        public Task<AuthenticationTicket?> RetrieveAsync(string key)
        {
            AuthenticationTicket? ticket;
            var bytes = cache.Get(key);
            ticket = DeserializeFromBytes(bytes ?? []) ?? default;
            return Task.FromResult(ticket);
        }

        public Task RemoveAsync(string key)
        {
            cache.Remove(key);
            return Task.FromResult(0);
        }

        private static byte[] SerializeToBytes(AuthenticationTicket source)
        {
            return TicketSerializer.Default.Serialize(source);
        }

        private static AuthenticationTicket? DeserializeFromBytes(byte[] source)
        {
            return (source is null) ? default : TicketSerializer.Default.Deserialize(source);
        }
    }
}
The previosly implemented Distributed Cache was already working correctly:
DistributedCacheComposer
Copy code
cs
public class DistributedCacheComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        if (ServerInfo.Role == ServerRole.SchedulingPublisher)
        {
            builder.AddNotificationHandler<UmbracoApplicationStartingNotification, RunAddCacheTableMigration>();
        }

        builder.Services.AddDistributedSqlServerCache(options =>
        {
            options.SchemaName = "dbo";
            options.TableName = nameof(CacheStore);
            options.ConnectionString = builder.Config.GetConnectionString("umbracoDbDSN");
        });
    }
}
IMPORTANT: We had to add
DataProtection
package which helped with sharing data protection keys between the pods
Cms.proj
Copy code
<PackageReference Include="Umbraco.Community.DataProtection" Version="13.0.0" />
Without that package, we only got sessions in the cache from each pod and they were not reused. Hope it helps someone.
k
The documentation for this https://docs.umbraco.com/umbraco-cms/fundamentals/setup/server-setup/load-balancing#session-state-and-distributed-cache is extremely lacking. This doc: > The distributed cache is used by the session in your application ...should probably say something like you wrote above, that you need to manually enable session state, and manually set the session store to a distributed system. The docs make it sound like Umbraco does this automatically. And also mentions
TempData
for some reason, which is only technically related to session state. I'm surprised you had to implement your own ticket store, what was the reason for that?
g
I couldn't find any existing implementation for that. Also chekced on Umbraco's github, but there is none. Every repo I was checking has this implemented on their own, even though it is quite standard thing. In the end I took very much what Bitwarden is using for that.
20 Views