title: ASP.Net Core SignalR authentication always responding with 403 - Forbidden
body: <h2>Summary</h2>
  
  <p>I am trying to add security/authentication to my SignalR hubs, but no matter what I try the client requests keep getting a 403 - Forbidden responses (despite the requests successfully authenticating).</p>
  
  <h2>Setup</h2>
  
  <p>My project is based on Microsoft's SignalRChat example from:</p>
  
  <p><a href="https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&amp;tabs=visual-studio" rel="nofollow noreferrer">https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&amp;tabs=visual-studio</a></p>
  
  <p>Basically I have an ASP.Net Core web application with Razor Pages.  The project is targeting .Net Core 3.1.</p>
  
  <p>The client library being used is v3.1.0 of Microsoft's JavaScript client library.</p>
  
  <p>I also referenced their authentication and authorization document for the security side:</p>
  
  <p><a href="https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1" rel="nofollow noreferrer">https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1</a></p>
  
  <p>The key difference is rather than using the JWT Bearer middleware, I made my own custom token authentication handler.</p>
  
  <h2>Code</h2>
  
  <p><strong>chat.js:</strong></p>
  
  <pre><code>"use strict";
  
  var connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub", { accessTokenFactory: () =&gt; 'mytoken' })
    .configureLogging(signalR.LogLevel.Debug)
    .build();
  
  //Disable send button until connection is established
  document.getElementById("sendButton").disabled = true;
  
  connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&amp;/g, "&amp;amp;").replace(/&lt;/g, "&amp;lt;").replace(/&gt;/g, "&amp;gt;");
    var encodedMsg = user + " says " + msg;
    var li = document.createElement("li");
    li.textContent = encodedMsg;
    document.getElementById("messagesList").appendChild(li);
  });
  
  connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
  }).catch(function (err) {
    return console.error(err.toString());
  });
  
  document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
      return console.error(err.toString());
    });
    event.preventDefault();
  });
  </code></pre>
  
  <p><strong>Startup.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Builder;
  using Microsoft.AspNetCore.Hosting;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Hosting;
  using SignalRChat.Hubs;
  using SignalRChat.Security;
  
  namespace SignalRChat
  {
    public class Startup
    {
      public Startup(IConfiguration configuration)
      {
        Configuration = configuration;
      }
  
      public IConfiguration Configuration { get; }
  
      // This method gets called by the runtime. Use this method to add services to the container.
      public void ConfigureServices(IServiceCollection services)
      {
        // Found other cases where CORS caused authentication issues, so
        // making sure that everything is allowed.
        services.AddCors(options =&gt;
        {
          options.AddPolicy("AllowAny", policy =&gt;
          {
            policy
            .WithOrigins("http://localhost:44312/", "https://localhost:44312/")
            .AllowCredentials()
            .AllowAnyHeader()
            .AllowAnyMethod();
          });
        });
  
        services
          .AddAuthentication()
          .AddHubTokenAuthenticationScheme();
  
        services.AddAuthorization(options =&gt;
        {
          options.AddHubAuthorizationPolicy();
        });
  
        services.AddRazorPages();
  
        services.AddSignalR(options =&gt;
        {
          options.EnableDetailedErrors = true;
        });
      }
  
      // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
      public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
      {
        if (env.IsDevelopment())
        {
          app.UseDeveloperExceptionPage();
        }
        else
        {
          app.UseExceptionHandler("/Error");
          // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
          app.UseHsts();
        }
  
        app.UseHttpsRedirection();
  
        app.UseStaticFiles();
  
        app.UseRouting();
  
        app.UseAuthentication();
  
        app.UseAuthorization();
  
        app.UseEndpoints(endpoints =&gt;
        {
          endpoints.MapRazorPages();
          endpoints.MapHub&lt;ChatHub&gt;("/chatHub");
        });
      }
    }
  }
  </code></pre>
  
  <p><strong>ChatHub.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Authorization;
  using Microsoft.AspNetCore.SignalR;
  using SignalRChat.Security;
  using System.Threading.Tasks;
  
  namespace SignalRChat.Hubs
  {
    [Authorize(HubRequirementDefaults.PolicyName)]
    public class ChatHub : Hub
    {
      public async Task SendMessage(string user, string message)
      {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
      }
    }
  }
  </code></pre>
  
  <p><strong>HubTokenAuthenticationHandler.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Authentication;
  using Microsoft.Extensions.Logging;
  using Microsoft.Extensions.Options;
  using System;
  using System.Security.Claims;
  using System.Text.Encodings.Web;
  using System.Threading.Tasks;
  
  namespace SignalRChat.Security
  {
    public class HubTokenAuthenticationHandler : AuthenticationHandler&lt;HubTokenAuthenticationOptions&gt;
    {
      public IServiceProvider ServiceProvider { get; set; }
  
      public HubTokenAuthenticationHandler(
        IOptionsMonitor&lt;HubTokenAuthenticationOptions&gt; options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IServiceProvider serviceProvider)
        : base(options, logger, encoder, clock)
      {
        ServiceProvider = serviceProvider;
      }
  
      protected override Task&lt;AuthenticateResult&gt; HandleAuthenticateAsync()
      {
        bool isValid = TryAuthenticate(out AuthenticationTicket ticket, out string message);
  
        if (isValid) return Task.FromResult(AuthenticateResult.Success(ticket));
  
        return Task.FromResult(AuthenticateResult.Fail(message));
      }
  
      private bool TryAuthenticate(out AuthenticationTicket ticket, out string message)
      {
        message = null;
        ticket = null;
  
        var token = GetToken();
  
        if (string.IsNullOrEmpty(token))
        {
          message = "Token is missing";
          return false;
        }
  
        bool tokenIsValid = token.Equals("mytoken");
  
        if (!tokenIsValid)
        {
          message = $"Token is invalid: token={token}";
          return false;
        }
  
        var claims = new[] { new Claim("token", token) };
        var identity = new ClaimsIdentity(claims, nameof(HubTokenAuthenticationHandler));
        ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
  
        return true;
      }
  
      #region Get Token
  
      private string GetToken()
      {
        string token = Request.Query["access_token"];
  
        if (string.IsNullOrEmpty(token))
        {
          token = GetTokenFromHeader();
        }
  
        return token;
      }
  
      private string GetTokenFromHeader()
      {
        string token = Request.Headers["Authorization"];
  
        if (string.IsNullOrEmpty(token)) return null;
  
        // The Authorization header value should be in the format "Bearer [token_value]"
        string[] authorizationParts = token.Split(new char[] { ' ' });
  
        if (authorizationParts == null || authorizationParts.Length &lt; 2) return token;
  
        return authorizationParts[1];
      }
  
      #endregion
    }
  }
  </code></pre>
  
  <p><strong>HubTokenAuthenticationOptions.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Authentication;
  
  namespace SignalRChat.Security
  {
    public class HubTokenAuthenticationOptions : AuthenticationSchemeOptions { }
  }
  </code></pre>
  
  <p><strong>HubTokenAuthenticationDefaults.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Authentication;
  using System;
  
  namespace SignalRChat.Security
  {
    public static class HubTokenAuthenticationDefaults
    {
      public const string AuthenticationScheme = "HubTokenAuthentication";
  
      public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
      {
        return AddHubTokenAuthenticationScheme(builder, (options) =&gt; { });
      }
  
      public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action&lt;HubTokenAuthenticationOptions&gt; configureOptions)
      {
        return builder.AddScheme&lt;HubTokenAuthenticationOptions, HubTokenAuthenticationHandler&gt;(AuthenticationScheme, configureOptions);
      }
    }
  }
  </code></pre>
  
  <p><strong>HubRequirement.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Authorization;
  using Microsoft.AspNetCore.SignalR;
  using System.Threading.Tasks;
  
  namespace SignalRChat.Security
  {
    public class HubRequirement : AuthorizationHandler&lt;HubRequirement, HubInvocationContext&gt;, IAuthorizationRequirement
    {
      protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
      {
        // Authorization logic goes here.  Just calling it a success for demo purposes.
        context.Succeed(requirement);
        return Task.CompletedTask;
      }
    }
  }
  </code></pre>
  
  <p><strong>HubRequirementDefaults.cs</strong></p>
  
  <pre><code>using Microsoft.AspNetCore.Authentication;
  using System;
  
  namespace SignalRChat.Security
  {
    public static class HubTokenAuthenticationDefaults
    {
      public const string AuthenticationScheme = "HubTokenAuthentication";
  
      public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
      {
        return AddHubTokenAuthenticationScheme(builder, (options) =&gt; { });
      }
  
      public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action&lt;HubTokenAuthenticationOptions&gt; configureOptions)
      {
        return builder.AddScheme&lt;HubTokenAuthenticationOptions, HubTokenAuthenticationHandler&gt;(AuthenticationScheme, configureOptions);
      }
    }
  }
  </code></pre>
  
  <h2>The Results</h2>
  
  <p>On the client side, I see the following errors in the browser's developer console:</p>
  
  <ul>
  <li>POST <a href="https://localhost:44312/chatHub/negotiate?negotiateVersion=1" rel="nofollow noreferrer">https://localhost:44312/chatHub/negotiate?negotiateVersion=1</a> 403</li>
  <li>Error: Failed to complete negotiation with the server: Error</li>
  <li>Error: Failed to start the connection: Error</li>
  </ul>
  
  <p>On the server side, all I see is:</p>
  
  <ul>
  <li>SignalRChat.Security.HubTokenAuthenticationHandler: Debug: AuthenticationScheme: HubTokenAuthentication was successfully authenticated.</li>
  <li>SignalRChat.Security.HubTokenAuthenticationHandler: Information:
  AuthenticationScheme: HubTokenAuthentication was forbidden.</li>
  </ul>
  
  <h2>Next Steps</h2>
  
  <p>I did see that others had issues with CORS preventing them from security from working, but I believe it usually said that explicitly in the client side errors.  Despite that, I added the CORS policies in Startup.cs that I believe <em>should</em> have circumvented that.</p>
  
  <p>I also experimented around with changing the order of service configurations in Startup, but nothing seemed to help.</p>
  
  <p>If I remove the Authorize attribute (i.e. have an unauthenticated hub) everything works fine.</p>
  
  <p>Finally, I found the server side messages to be very interesting in that the authentication succeeded, yet the request was still forbidden.</p>
  
  <p>I'm not really sure where to go from here.  Any insights would be most appreciated.</p>
  
  <h2>Update</h2>
  
  <p>I have been able to debug this a little bit.</p>
  
  <p>By loading system symbols and moving up the call stack, I found my way to Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator:</p>
  
  <p><a href="https://i.stack.imgur.com/xK4a2.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/xK4a2.png" alt="PolicyEvaluator Authentication Results"></a></p>
  
  <p>As can be seen, the authentication succeeded but apparently the authorization did not.  Looking at the requirements, there are two: a DenyAnonymousAuthorizationRequirement and my HubRequirement (which automatically succeeds).</p>
  
  <p>Because the debugger never hit my breakpoint in my HubRequirement class, I am left to assume that the DenyAnonymousAuthorizationRequirement is what is failing.  Interesting, because based on the code listing on <a href="https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs" rel="nofollow noreferrer">github</a> (<a href="https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs" rel="nofollow noreferrer">https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs</a>) I should be meeting all the requirements:</p>
  
  <p><a href="https://i.stack.imgur.com/uY65B.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/uY65B.png" alt="PolicyEvaluator context User members"></a></p>
  
  <p>There is a User defined on the context, the user has an identity, and there are no identities that are unauthenticated.</p>
  
  <p>I have to be missing something, because this isn't adding up.</p>