Skip to content

Commit

Permalink
Merge pull request #369 from NielsPilgaard/bugfix/session-affinity
Browse files Browse the repository at this point in the history
Try to support session affinity in SignalR Hub connection
  • Loading branch information
NielsPilgaard authored May 12, 2024
2 parents 6db2550 + 5534f85 commit 9e0245f
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static class CircuitHandlerExtensions
public static IServiceCollection AddCurrentUser(this IServiceCollection services)
{
services.AddScoped<CurrentUser>();
services.AddScoped<CookieContainerFactory>();
services.AddScoped<CookieFactory>();
services.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
return services;
}
Expand Down
29 changes: 0 additions & 29 deletions src/web/Jordnaer/Features/Authentication/CookieContainerFactory.cs

This file was deleted.

20 changes: 20 additions & 0 deletions src/web/Jordnaer/Features/Authentication/CookieFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Net;

namespace Jordnaer.Features.Authentication;

public sealed class CookieFactory(ILogger<CookieFactory> logger)
{
public Cookie Create(string name, string cookie, string domain)
{
logger.LogDebug("Creating cookie with domain {Domain}", domain);

return new Cookie(name: name,
value: cookie,
path: "/",
domain: domain)
{
Secure = true,
HttpOnly = true
};
}
}
46 changes: 30 additions & 16 deletions src/web/Jordnaer/Features/Authentication/UserCircuitHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Jordnaer.Extensions;
using System.Net;
using Jordnaer.Extensions;
using Jordnaer.Features.Profile;
using Jordnaer.Shared;
using Microsoft.AspNetCore.Components.Authorization;
Expand All @@ -12,7 +13,7 @@ internal sealed class UserCircuitHandler(
IProfileCache profileCache,
ILogger<UserCircuitHandler> logger,
IHttpContextAccessor httpContextAccessor,
CookieContainerFactory cookieContainerFactory)
CookieFactory cookieFactory)
: CircuitHandler, IDisposable
{
public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken)
Expand Down Expand Up @@ -52,37 +53,50 @@ async Task UpdateAuthentication(Task<AuthenticationState> task)

public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
if (currentUser.CookieContainer is not null)
// If user's circuit already has a cookie container with all our known cookies, return early.
if (currentUser.CookieContainer?.Count is > 3)
{
logger.LogDebug("CurrentUser already has a Cookie Container, returning.");
return Task.CompletedTask;
}

if (httpContextAccessor.HttpContext is null)
if (currentUser.Id is null)
{
logger.LogWarning("No HttpContext is associated with Circuit {CircuitId}", circuit.Id);
logger.LogTrace("CurrentUser is not logged in yet, returning.");
return Task.CompletedTask;
}

if (!httpContextAccessor.HttpContext.Request.Cookies
.TryGetValue(AuthenticationConstants.CookieName, out var cookie))
if (httpContextAccessor.HttpContext is null)
{
if (currentUser.Id is not null)
{
logger.LogError("Failed to get cookie by name '{CookieName}' by logged in User {UserId}", AuthenticationConstants.CookieName, currentUser.Id);
}

// User is not yet logged in, return early.
logger.LogWarning("No HttpContext is associated with Circuit {CircuitId}", circuit.Id);
return Task.CompletedTask;
}

var domain = httpContextAccessor.HttpContext.Request.Host.Host;

var cookieContainer = cookieContainerFactory.Create(cookie, domain);
currentUser.CookieContainer = new CookieContainer();

currentUser.CookieContainer = cookieContainer;
string[] cookieNames = ["ARRAffinity", "ARRAffinitySameSite", AuthenticationConstants.CookieName];

foreach (var cookieName in cookieNames)
{
if (httpContextAccessor.HttpContext.Request.Cookies.TryGetValue(cookieName, out var sessionAffinityCookie))
{
currentUser.CookieContainer.Add(
cookieFactory.Create(name: cookieName,
cookie: sessionAffinityCookie,
// Session Affinity Cookies have a "." as prefix
domain: cookieName.StartsWith("ARR") ? $".{domain}" : domain));
}
else
{
logger.LogWarning("Failed to get cookie by name '{CookieName}' " +
"for logged in User {UserId}. SignalR Connection may be in a broken state.",
cookieName, currentUser.Id);
}
}

logger.LogDebug("Successfully set cookie for User {UserId}", currentUser.Id);
logger.LogDebug("Finished setting cookies for User {UserId}", currentUser.Id);

return Task.CompletedTask;
}
Expand Down

0 comments on commit 9e0245f

Please sign in to comment.