ASP. Net Аутентификация Core SignalR всегда отвечает 403 - Запрещено - PullRequest
1 голос
/ 30 января 2020

Сводка

Я пытаюсь добавить безопасность / аутентификацию в мои концентраторы SignalR, но независимо от того, что я пытаюсь, клиентские запросы продолжают получать 403 - Запрещенные ответы (несмотря на успешную аутентификацию запросов).

Настройка

Мой проект основан на примере Microsoft SignalRChat из:

https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio

В основном у меня ASP. Net Базовое веб-приложение с Razor Pages. Проект нацелен. Net Core 3.1.

Используемая клиентская библиотека v3.1.0 из JavaScript клиентской библиотеки Microsoft.

Я также ссылался на их документ по аутентификации и авторизации для сторона безопасности:

https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1

Разница в ключе заключается не в том, чтобы использовать промежуточное ПО JWT Bearer, я создал свой собственный обработчик аутентификации токена.

Код

в чате. js:

"use strict";

var connection = new signalR.HubConnectionBuilder()
  .withUrl("/chatHub", { accessTokenFactory: () => '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(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&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();
});

Startup.cs

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 =>
      {
        options.AddPolicy("AllowAny", policy =>
        {
          policy
          .WithOrigins("http://localhost:44312/", "https://localhost:44312/")
          .AllowCredentials()
          .AllowAnyHeader()
          .AllowAnyMethod();
        });
      });

      services
        .AddAuthentication()
        .AddHubTokenAuthenticationScheme();

      services.AddAuthorization(options =>
      {
        options.AddHubAuthorizationPolicy();
      });

      services.AddRazorPages();

      services.AddSignalR(options =>
      {
        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 =>
      {
        endpoints.MapRazorPages();
        endpoints.MapHub<ChatHub>("/chatHub");
      });
    }
  }
}

ChatHub.cs

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);
    }
  }
}

HubTokenAuthenticationHandler.cs

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<HubTokenAuthenticationOptions>
  {
    public IServiceProvider ServiceProvider { get; set; }

    public HubTokenAuthenticationHandler(
      IOptionsMonitor<HubTokenAuthenticationOptions> options,
      ILoggerFactory logger,
      UrlEncoder encoder,
      ISystemClock clock,
      IServiceProvider serviceProvider)
      : base(options, logger, encoder, clock)
    {
      ServiceProvider = serviceProvider;
    }

    protected override Task<AuthenticateResult> 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 < 2) return token;

      return authorizationParts[1];
    }

    #endregion
  }
}

HubTokenAuthenticationOptions.cs

using Microsoft.AspNetCore.Authentication;

namespace SignalRChat.Security
{
  public class HubTokenAuthenticationOptions : AuthenticationSchemeOptions { }
}

HubTokenAuthenticationDefaults.cs

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) => { });
    }

    public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
    {
      return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
    }
  }
}

HubRequirement.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRChat.Security
{
  public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, 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;
    }
  }
}

HubRequirementDefaults.cs

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) => { });
    }

    public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
    {
      return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
    }
  }
}

Результаты

На стороне клиента я вижу следующие ошибки в консоли разработчика браузера:

На стороне сервера все, что я вижу, это:

  • SignalRChat.Security.HubTokenAuthenticationHandler: Отладка: AuthenticationScheme: HubTokenAuthentication была успешно аутентифицирована.
  • SignalRChat.Security.HubTokenutut: : AuthenticationScheme: HubTokenAuthentication был запрещен.

Следующие шаги

Я действительно видел, что у других были проблемы с CORS, мешавшие им работать в безопасности, но я полагаю, что это обычно говорилось явно в ошибки на стороне клиента. Несмотря на это, я добавил в файл Startup.cs политики CORS, которые, по моему мнению, должны обойти это.

Я также экспериментировал с изменением порядка конфигураций служб в Startup, но, похоже, ничего не изменилось. help.

Если я удаляю атрибут Authorize (т. е. имею хаб без аутентификации), все работает нормально.

Наконец, я обнаружил, что сообщения на стороне сервера очень интересны тем, что аутентификация прошла успешно, но пока запрос все еще был запрещен.

Я не совсем уверен, где отсюда go. Любые идеи будут наиболее цениться.

Обновление

Я смог немного отладить это.

Загружая системные символы и перемещаясь вверх по стеку вызовов, я нашел Мой путь к Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator:

PolicyEvaluator Authentication Results

Как видно, аутентификация прошла успешно, но, видимо, авторизация не прошла. Если посмотреть на требования, их два: DenyAnonymousAuthorizationRequirement и мой HubRequirement (который автоматически выполняется).

Поскольку отладчик никогда не достигает моей точки останова в моем классе HubRequirement, я могу предположить, что DenyAnonymousAuthorizationRequirement является причиной сбоя , Интересно, потому что, основываясь на листинге кода на github (https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs), я должен отвечать всем требованиям:

PolicyEvaluator context User members

В контексте определен пользователь, у пользователя есть удостоверение, и нет удостоверений, которые не прошли проверку подлинности.

Я должен что-то упустить, потому что это не складывается .

Ответы [ 2 ]

2 голосов
/ 12 марта 2020

Спасибо, сэкономил мне много часов отладки!

Похоже, проблема в том, что HandleAsyn c также вызывается с ресурсом RouteEndpoint для сигнализатора root и URL-адресами согласования, случай базовый класс не обрабатывает, и поскольку никакой обработчик авторизации не сообщает об успешном завершении, он терпит неудачу.

   public override async Task HandleAsync(AuthorizationHandlerContext context)
    {
        if (context.Resource is HubInvocationContext)
        {
            foreach (var req in context.Requirements.OfType<RealtimeHubSecurityAuthorizationHandler>())
            {
                await HandleRequirementAsync(context, req, (HubInvocationContext)context.Resource);
            }
        } else if (context.Resource is Microsoft.AspNetCore.Routing.RouteEndpoint) {
            //allow signalr root and negotiation url
            context.Succeed(this);
        }
    }

(опубликовано как ответ, поскольку длина комментария ограничена, извините)

2 голосов
/ 07 февраля 2020

Оказывается, что сбой действительно происходил в моем классе HubRequirement, а не в DenyAnonymousAuthorizationRequirement.

Хотя мой класс HubRequirement реализовал HandleRequirementAsyn c (), он не реализовал HandleAsyn c (), то есть вместо этого произошло то, что было названо.

Если я обновлю свой класс HubRequirement до следующего, все будет работать как положено:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRChat.Security
{
  public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
  {
    public override Task HandleAsync(AuthorizationHandlerContext context)
    {
      foreach (var requirement in context.PendingRequirements)
      {
        // TODO: Validate each requirement
      }

      // Authorization logic goes here.  Just calling it a success for demo purposes.
      context.Succeed(this);
      return Task.CompletedTask;
    }

    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;
    }
  }
}
...