AspNet.Core, IdentityServer 4: Несанкционированный (401) во время рукопожатия через веб-сокет с SignalR 1.0 с использованием токена носителя JWT

У меня есть две службы aspnet.core. Один для IdentityServer 4 и один для API, используемого клиентами Angular4 +. Хаб SignalR работает на API. Все решение работает на докере, но это не имеет значения (см. Ниже).

Я использую неявный поток аутентификации, который работает безупречно. Приложение NG перенаправляет на страницу входа в IdentityServer, где пользователь входит в систему. После этого браузер перенаправляется обратно в приложение NG с токеном доступа. Затем токен используется для вызова API и установления связи с SignalR. Думаю, я прочитал все, что доступно (см. Источники ниже).

Поскольку SignalR использует веб-сокеты, которые не поддерживают заголовки, токен следует отправлять в строке запроса. Затем на стороне API токен извлекается и устанавливается для запроса так же, как и в заголовке. Затем токен проверяется, и пользователь авторизуется.

API работает без проблем, пользователи авторизуются, а заявки могут быть получены на стороне API. Так что проблем с IdentityServer быть не должно, поскольку SignalR не требует специальной настройки. Я прав?

Когда я не использую атрибут [Authorized] в концентраторе SignalR, рукопожатие успешно. Вот почему я думаю, что в используемой мной инфраструктуре докеров и обратном прокси-сервере нет ничего плохого (прокси настроен на включение веб-сокетов).

Итак, без авторизации SignalR работает. При авторизации клиент NG во время рукопожатия получает следующий ответ:

Failed to load resource: the server responded with a status of 401
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error

Запрос

Request URL: https://publicapi.localhost/context/negotiate?signalr_token=eyJhbGciOiJSUz... (token is truncated for simplicity)
Request Method: POST
Status Code: 401 
Remote Address: 127.0.0.1:443
Referrer Policy: no-referrer-when-downgrade

В ответ я получаю:

access-control-allow-credentials: true
access-control-allow-origin: http://localhost:4200
content-length: 0
date: Fri, 01 Jun 2018 09:00:41 GMT
server: nginx/1.13.10
status: 401
vary: Origin
www-authenticate: Bearer

Согласно журналам, токен успешно проверен. Я могу включить полные журналы, но подозреваю, в чем проблема. Я включу эту часть сюда:

[09:00:41:0561 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
[09:00:41:0564 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.

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

services.AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddIdentityServerAuthentication(options =>
        {
            options.Authority = "http://identitysrv";
            options.RequireHttpsMetadata = false;
            options.ApiName = "publicAPI";
            options.JwtBearerEvents.OnMessageReceived = context =>
            {
                if (context.Request.Query.TryGetValue("signalr_token", out StringValues token))
                {
                    context.Options.Authority = "http://identitysrv";
                    context.Options.Audience = "publicAPI";
                    context.Token = token;
                    context.Options.Validate();
                }

                return Task.CompletedTask;
            };
        });

Других ошибок, исключений в системе нет. Я могу отладить приложение, и вроде все в порядке.

Что означают включенные строки журнала? Как я могу отладить происходящее во время авторизации?

РЕДАКТИРОВАТЬ: Я почти забыл упомянуть, что я думал, что проблема связана со схемами аутентификации, поэтому я установил для каждой схемы ту, которая, по моему мнению, была необходима. Однако, к сожалению, это не помогло.

Я здесь немного невежественен, поэтому ценю любое предложение. Спасибо.

Источники информации:

Передать токен аутентификации в SignalR

Защита SignalR с помощью IdentityServer

Microsoft docs по авторизации SignalR

Еще один вопрос по GitHub

Аутентифицироваться с помощью SignalR

Identity.Application не аутентифицирован


person Daniel Leiszen    schedule 01.06.2018    source источник


Ответы (3)


Я должен ответить на свой вопрос, потому что у меня был крайний срок, и, как ни странно, мне удалось его решить. Поэтому я записываю его в надежде, что он кому-то поможет в будущем.

Сначала мне нужно было понять, что происходит, поэтому я заменил весь механизм авторизации на свой собственный. Я мог сделать это с помощью этого кода. Это не требуется для решения, однако, если кому-то это нужно, это способ сделать.

services.Configure<AuthenticationOptions>(options =>
{
    var scheme = options.Schemes.SingleOrDefault(s => s.Name == JwtBearerDefaults.AuthenticationScheme);
    scheme.HandlerType = typeof(CustomAuthenticationHandler);
});

С помощью IdentityServerAuthenticationHandler, наконец, понял, что все возможные методы OnAuthenticationHandler и IdentityServerAuthenticationHandler наконец-то поняли, что событие выполняется после проверки токена. Поэтому, если бы во время вызова HandleAuthenticateAsync не было токена, ответ был бы 401. Это помогло мне выяснить, где разместить свой собственный код.

Мне нужно было реализовать свой собственный протокол во время получения токена. Я заменил этот механизм.

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;                
}).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme,
    options =>
    {
        options.Authority = "http://identitysrv";
        options.TokenRetriever = CustomTokenRetriever.FromHeaderAndQueryString;
        options.RequireHttpsMetadata = false;
        options.ApiName = "publicAPI";
    });

Важной частью является свойство TokenRetriever и то, что его заменяет.

public class CustomTokenRetriever
{
    internal const string TokenItemsKey = "idsrv4:tokenvalidation:token";
    // custom token key change it to the one you use for sending the access_token to the server
    // during websocket handshake
    internal const string SignalRTokenKey = "signalr_token";

    static Func<HttpRequest, string> AuthHeaderTokenRetriever { get; set; }
    static Func<HttpRequest, string> QueryStringTokenRetriever { get; set; }

    static CustomTokenRetriever()
    {
        AuthHeaderTokenRetriever = TokenRetrieval.FromAuthorizationHeader();
        QueryStringTokenRetriever = TokenRetrieval.FromQueryString();
    }

    public static string FromHeaderAndQueryString(HttpRequest request)
    {
        var token = AuthHeaderTokenRetriever(request);

        if (string.IsNullOrEmpty(token))
        {
            token = QueryStringTokenRetriever(request);
        }

        if (string.IsNullOrEmpty(token))
        {
            token = request.HttpContext.Items[TokenItemsKey] as string;
        }

        if (string.IsNullOrEmpty(token) && request.Query.TryGetValue(SignalRTokenKey, out StringValues extract))
        {
            token = extract.ToString();
        }

        return token;
    }

И это мой собственный алгоритм извлечения токенов, который сначала пробует стандартный заголовок и строку запроса для поддержки распространенных ситуаций, таких как вызовы веб-API. Но если токен все еще пуст, он пытается получить его из строки запроса, куда клиент поместил его во время рукопожатия веб-сокета.

РЕДАКТИРОВАТЬ: я использую следующий код на стороне клиента (TypeScript), чтобы предоставить токен для рукопожатия SignalR

import { HubConnection, HubConnectionBuilder, HubConnectionState } from '@aspnet/signalr';
// ...
const url = `${apiUrl}/${hubPath}?signalr_token=${accessToken}`;
const hubConnection = new HubConnectionBuilder().withUrl(url).build();
await hubConnection.start();

Где apiUrl, hubPath и accessToken - обязательные параметры соединения.

person Daniel Leiszen    schedule 02.06.2018
comment
v. интересно. У меня аналогичная проблема. как вы добавили собственный заголовок в клиентский JS (или TS)? вы вносили изменения в пакет SignalR npm? Я не вижу ничего в api для передачи заголовка - спасибо - person ChrisCa; 05.06.2018
comment
@ChrisCa: для этой цели мы используем HttpInterceptor (TS, Angular4 +). Я могу поделиться кодом, если хотите, однако мы используем rxjs и redux, поэтому процесс распределяется между разными компонентами. Но вы можете найти пример реализации почти для каждого технологического стека. - person Daniel Leiszen; 06.06.2018
comment
Привет спасибо. Теперь я понимаю, что вы передаете токен аутентификации в строке запроса, верно? У меня это уже работает. Я думал, вы придумали способ использовать заголовок авторизации вместо строки запроса, но я не думаю, что вы это делаете? - person ChrisCa; 06.06.2018
comment
Нет, к сожалению, AFAIK это невозможно в моем случае. Но технология позволяет это (см. developer.mozilla. org / en-US / docs / Web / API / WebSockets_API /). Итак, если вы можете изменить заголовок для веб-сокета, это должно работать с IdentityServer4. - person Daniel Leiszen; 11.06.2018
comment
да, похоже, браузеры не позволяют устанавливать заголовок, хотя вы можете это сделать с других типов клиентов. к сожалению, я нахожусь в браузере, поэтому мне нужно использовать строку запроса - хотя еще раз спасибо - person ChrisCa; 15.06.2018
comment
Большое спасибо, Даниэль :) Я преследовал ту же проблему, пока не увидел твое решение !!! - person Nabil.A; 08.09.2018
comment
@DanielLeiszen есть ли проблемы с безопасностью, связанные с передачей токена через запрос с использованием веб-сокетов? - person Anton Toshik; 09.09.2018
comment
@AntonToshik AFAIK, строка запроса так же видна, как и заголовок запроса, так что это скорее решение протокола, чем решение безопасности. используя HTTPS, строка запроса достаточно безопасна, я думаю - person Daniel Leiszen; 01.10.2018
comment
@DanielLeiszen Как выглядит код подключения на стороне клиента сигнализации? - person ScottFoster1000; 21.12.2018
comment
Спасибо, я видел много примеров, это единственное, что у меня сработало. - person Hasni Mehdi; 20.12.2019
comment
Спасибо, Даниэль :-) Он работает с asp core 3.1 и IdentityServer4. Отличный вопрос и ответ. - person slmglr; 25.04.2020

Я знаю, что это старая ветка, но на случай, если кто-то наткнется на это, как я. Нашел альтернативное решение.

TL; DR: JwtBearerEvents.OnMessageReceived перехватит токен до его проверки при использовании, как показано ниже:

public void ConfigureServices(IServiceCollection services)
{
    // Code removed for brevity
    services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = "https://myauthority.io";
        options.ApiName = "MyApi";
        options.JwtBearerEvents = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];

                // If the request is for our hub...
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) &&
                    (path.StartsWithSegments("/hubs/myhubname")))
                {
                    // Read the token out of the query string
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });
}

Этот документ Microsoft подсказал мне: https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1. Однако в примере Microsoft вызывается options.Events, поскольку это не пример использования IdentityServerAuthentication. Если options.JwtBearerEvents используется так же, как options.Events в примере Microsoft, IdentityServer4 счастлив!

person Alex Gemma    schedule 24.03.2020
comment
Это должен быть правильный ответ. Мне также нужно было добавить атрибут Authorize в SignalR Hub. - person Jim Culverwell; 20.04.2020
comment
В то время, когда я отвечал на этот вопрос, лучшего способа решить эту проблему не было. Тем временем, благодаря этому сообщению и проблеме с github, MS удалось решить реальную проблему, стоящую за этой проблемой. Я не знаю, какова фактическая политика, позволяющая обновлять ответ без изменения репутации. Какие-либо предложения? - person Daniel Leiszen; 25.04.2020
comment
Кроме того, в вопросе указывается фактическая версия SignalR, которая доступна в то время. Я думаю, что SignalR 2.0 принес много решений в этой области. - person Daniel Leiszen; 25.04.2020
comment
Я понимаю, что ты говоришь, Даниэль. Это был мой первый пост здесь, и я не пытался повлиять на вашу репутацию. Я просто пытался поделиться новым решением. Может быть, я мог бы предложить отредактировать ваш ответ, используя это как другое возможное решение, чтобы ваша репутация не пострадала? - person Alex Gemma; 26.04.2020
comment
@AlexGemma: Я тоже не знаю, какой протокол должен быть в этой ситуации. Достоверность информации во времени всегда была слабым местом Интернета. И я не вижу ни одной особенности этого в SO. Я не беспокоюсь о своей репутации, я думаю, что голосование за ответ может помочь. Как я вижу из комментариев, в некоторых ситуациях этот ответ не работает даже с aspnet.core 3.1, но мой исходный ответ все еще работает. - person Daniel Leiszen; 26.04.2020
comment
Я не вижу примера, чтобы этот ответ не работал в ASP.NET Core 3.1. В вашем вопросе ваш исходный код на самом деле был очень близок к этому ответу. Основное различие, которое я вижу, заключается в следующем: options.JwtBearerEvents = new JwtBearerEvents {OnMessagedReceived ... vs options.JwtBearerEvents.OnMessageReceived ... Я предлагаю отредактировать, чтобы включить этот параметр в выбранный ответ, но в том случае, если он не принимается, этот ответ все еще может быть здесь, как еще один вариант. Я ценю сотрудничество и голосование "за"! - person Alex Gemma; 27.04.2020
comment
fyi, не передавайте префикс Bearer с токеном - person MIKE; 24.07.2020
comment
@AlexGemma какую версию сигнализатора вы используете? Я использую AspNetCore.SignalR.Core 1.1.0, но у меня это все еще не сработало. Мне пришлось пойти по пути, предложенному в этом посте. stackoverflow.com/a/55926126/6012566 - person Amir; 12.09.2020
comment
@Amir Это ASP.NET Core 3.1. Да, вам также понадобится UseAuthentication (). - person Alex Gemma; 24.11.2020

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

Для этого добавьте ниже последний if оператор:

if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
    token = cookieToken;
}

На самом деле мы могли вообще удалить извлечение из строки запроса, как указано в Microsoft docs это не совсем безопасно, и его можно где-нибудь зарегистрировать.

person Eugene Chybisov    schedule 05.09.2019
comment
Спасибо за обновления. В документе, на который вы ссылаетесь, говорится, что при использовании WebSockets или событий, отправленных сервером, клиент браузера отправляет токен доступа в строке запроса. Получение токена доступа через строку запроса обычно так же безопасно, как и использование стандартного заголовка авторизации. Вы всегда должны использовать HTTPS, чтобы обеспечить безопасное сквозное соединение между клиентом и сервером. Об этом я тоже сказал в комментарии. При использовании аутентификации токена-носителя файлы cookie не используются. - person Daniel Leiszen; 07.09.2019
comment
@DanielLeiszen да, но вы почти всегда храните свой токен на предъявителя в cookie, поэтому мы можем использовать cookie для получения токена при рукопожатии. Это обычная практика. - person Eugene Chybisov; 07.09.2019
comment
вы имеете в виду, что сервер отправляет токен доступа / обновления, который затем сохраняется в файле cookie на стороне клиента и автоматически отправляется обратно во время запросов как часть пакета? Мне не известно о таком поведении, но я был бы рад увидеть несколько примеров. как создается cookie? - person Daniel Leiszen; 08.09.2019
comment
@DanielLeiszen Например, у вас есть IdentityServer4 и клиентский SPA. Вы аутентифицируетесь по ID4 и перенаправляетесь на клиентский SPA с помощью access_token. (Я рекомендую использовать поток кода авторизации в качестве последнего предложения OAuth 2.0 для SPA) Затем в SPA вы сохраняете свой токен в файле cookie. И после этого пытаешься подключиться к signalR. Cookie будет автоматически отправляться на сервер каждый раз во время рукопожатия, а затем вы перехватываете их с помощью класса поиска и получаете токен. После этого у вас будет токен так же, как через строку запроса, но чуть более красивым способом. - person Eugene Chybisov; 09.09.2019
comment
@DanielLeiszen Одна вещь, на которую следует обратить внимание - cookie всегда автоматически отправляет на сервер во время HTTP-запроса, вот как они работают. Для этого не нужно делать что-то дополнительно. Но вы, наверное, это уже знаете :) - person Eugene Chybisov; 09.09.2019
comment
Файлы cookie сначала отправляются С сервера. Если сервер не настроен на отправку файлов cookie, отправлять обратно нечего. Использование только токенов на предъявителя не требует файлов cookie, поэтому отправлять обратно нечего. - person Daniel Leiszen; 17.09.2019
comment
@DanielLeiszen изначально вы получаете токен не из cookie, который был получен с сервера, вы получаете токен из ответа конечной точки токена в формате json, и только после этого вы помещаете токен в cookie и сможете отправить их на сервер. - person Eugene Chybisov; 17.09.2019
comment
Спасибо за разъяснения. Таким образом, cookie создается на стороне клиента и отправляется на сервер. Таким образом, в отличие от запроса со строкой aproach, файлы cookie могут не регистрироваться переходами во время связи. Однако без HTTPS это так же уязвимо, поскольку содержит текстовое значение токена доступа. На HTTPS этот подход безопаснее, чем строка запроса. Я прав? - person Daniel Leiszen; 24.09.2019
comment
@DanielLeiszen точно :) Я надеюсь, что в наши дни никто не рассматривает возможность использования HTTP без S, так что я думаю, это можно считать правильным подходом. - person Eugene Chybisov; 25.09.2019