Search code examples
c#asp.netsignalrasp.net-identity

ASP.Net Core Authentication SignalR and Cors


In my current setup I am using a SPA (ReactJS) running on a different machine than my ASP.net Core Server. The whole client-server communication runs through SignalR except authentication. For security I want to use cookie-based authentication. As user-management I am using ASP.net.Core.Identity. Setting up CORS works and now I'm implementing a login/logout mechanism based on simple HTTP-Request.

Here the workflow:

  1. Send SignIn-Request to server via simple HTTP. (Works)
  2. After successfully signin set cookie on client. (Works)
  3. Client starts SignalR hub-connections (using the provided cookie). (Works)
  4. Client sends data to server via HubConnetion. (Works)
  5. Server sends data based on Claimspricipal. (Fails)

After the successfull authentication the client receives the cookie and uses it when negotiating with the signalR-hubs. When calling other controller-endpoints I have access to the ClaimsPrincipal which authenticated previously.

Now to my problem: when accessing the HubCallerContext the ClaimsPrincipal is not set (identity nor claims are set). Do I need to register the ClaimsPrincipal on this context as well or will it be handled by ASP.net? Other examples about authentication are assumuming that caller and hub are running on the same server. Did I missed or missunderstood something?

Here my Startup.cs:

public void ConfigureServices(IServiceCollection services)
{        
    services.AddScoped<SignInManager<ApplicationUser>, ApplicationSignInManager>();
    services.AddScoped<IAuthorizationHandler, MyRequirementHandler>();
    services.AddCors(
                options => options.AddPolicy("CorsPolicy",
                    builder =>
                    {
                        builder
                            .SetIsOriginAllowed(origin =>
                            {
                                if (string.IsNullOrEmpty(origin)) return false;
                                if (origin.ToLower().StartsWith("http://localhost")) return true;
                                return false;
                            })
                            .AllowAnyHeader()
                            .AllowCredentials();
                    })
            );
    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
    services.AddAuthorization(options =>
        {
            options.AddPolicy("PolicyName",
                policy => { policy.Requirements.Add(new MyRequirement()); });
        });

    services.ConfigureApplicationCookie(config =>
    {
        config.Cookie.HttpOnly = false;
        config.Cookie.Name = "my-fav-cookie";
        config.ExpireTimeSpan = TimeSpan.FromDays(14);
        config.Cookie.SameSite = SameSiteMode.None;
        config.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        config.SlidingExpiration = false;
    });

    services.AddSignalR(opt => { opt.EnableDetailedErrors = true; })
        .AddMessagePackProtocol(option => { option.SerializerOptions.WithResolver(StandardResolver.Instance); })
        .AddNewtonsoftJsonProtocol(option =>
        {
            option.PayloadSerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
            option.PayloadSerializerSettings.DateParseHandling = DateParseHandling.DateTime;
        });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger,
    ILoggerFactory loggerFactory)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseCors("CorsPolicy");
    app.UseRouting();

    app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        ...some Hubendpoints...
    });
}

Here the MyRequirement.cs:

public class MyRequirement : IAuthorizationRequirement
{
}

And here the MyRequirementHandler.cs:

public class
        MyRequirementHandler: AuthorizationHandler<MyRequirement,
            HubInvocationContext>
    {
        private readonly IAuthorizationHelper _authorizationHelper;
        private readonly UserManager<ApplicationUser> _userManager;

        public MyRequirementHandler(
            IAuthorizationHelper authorizationHelper,
            UserManager<ApplicationUser> userManager)
        {
            _authorizationHelper = authorizationHelper;
            _userManager = userManager;
        }

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
            MyRequirement requirement,
            HubInvocationContext resource)
        {
           var user = context.User;
           ...
        }
    }

And the Hub.cs:

public class Hub: Hub<ISystemClient>
{
    private readonly ISystemService _systemService;


    public Hub(ISystemService systemService)
    {
        _systemService = systemService;
    }

    [Authorize("PolicyName")]
    public async Task DoSthFancy()
    {
        var user = Context.User;
        var fancyStruff = await _systemService.GetFancyStuff(user);
        await Clients.Caller.SendFancyStuff(fancyStuff);
    }
}

Thanks for your help!

EDIT (04-11-2022) I edited/fixed the code-snippet of the Startup.cs due to bad copy-paste. Thanks for the hint.


Solution

  • After implementing an example step by step I figured out that ASP sets the ClaimsPrincipal inside the HubContext only when you are challeging the hub-endpoint or the hub itself by adding a [Authorization] attribute. So what was missing was a implementation of an AuthenticationHandler, a registration in the configuration and setting the authorization-attribute.

    public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
        {
            private readonly UserManager<ApplicationUser> _userManager;
            private readonly SignInManager<ApplicationUser> _signInManager;
    
            public CustomAuthenticationHandler(
                IOptionsMonitor<AuthenticationSchemeOptions> options,
                ILoggerFactory logger,
                UrlEncoder encoder,
                ISystemClock clock,
                UserManager<ApplicationUser> userManager,
                SignInManager<ApplicationUser> signInManager) : base(options, logger, encoder, clock)
            {
                _userManager = userManager;
                _signInManager = signInManager;
            }
    
            protected override Task<AuthenticateResult> HandleAuthenticateAsync()
            {
                if (Context.Request.Cookies.TryGetValue("my-fav-cookie", out var cookie))
                {
                    var user = Context.Request.HttpContext.User;
    
                    var ticket = new AuthenticationTicket(user, new(), CustomCookieScheme);
                    return Task.FromResult(AuthenticateResult.Success(ticket));
                }
    
                return Task.FromResult(AuthenticateResult.Fail("my-fav-cookie"));
            }
        }
    

    In the Startup.cs you need to register the Authentication:

         services.AddAuthentication().AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(
                    "MyCustomScheme", _ => { });
    

    and in the Hub:

    namespace My.HubProject {
    
        [Authorize(AuthenticationScheme="MyCustomScheme")]
        public class MyHub : Hub {
            ...
        }
    }
    

    Hope it will help others!