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 не аутентифицировано

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
0
5 223
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

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

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

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

С помощью IdentityServerAuthenticationHandler и переопределения всех возможных методов я наконец понял, что событие OnMessageRecced выполняется после проверки токена. Поэтому, если бы во время вызова 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 - обязательные параметры соединения.

v. интересно. У меня аналогичная проблема. как вы добавили собственный заголовок в клиентский JS (или TS)? вы вносили изменения в пакет SignalR npm? Я не вижу ничего в api для передачи заголовка - спасибо

ChrisCa 05.06.2018 10:40

@ChrisCa: для этой цели мы используем HttpInterceptor (TS, Angular4 +). Я могу поделиться кодом, если хотите, однако мы используем rxjs и redux, поэтому процесс распределяется между разными компонентами. Но вы можете найти пример реализации почти для каждого технологического стека.

Daniel Leiszen 06.06.2018 12:41

Привет спасибо. Теперь я понимаю, что вы передаете токен аутентификации в строке запроса, верно? У меня это уже работает. Я думал, вы придумали способ использовать заголовок авторизации вместо строки запроса, но я не думаю, что вы это делаете?

ChrisCa 06.06.2018 13:09

Нет, к сожалению, AFAIK это невозможно в моем случае. Но технология позволяет это (см .: developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/…). Итак, если вы можете изменить заголовок для веб-сокета, это должно работать с IdentityServer4.

Daniel Leiszen 11.06.2018 01:11

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

ChrisCa 15.06.2018 16:34

Большое спасибо, Даниэль :) Я преследовал ту же проблему, пока не увидел твое решение !!!

Nabil.A 08.09.2018 22:15

@DanielLeiszen есть ли проблемы с безопасностью, связанные с передачей токена через запрос с использованием веб-сокетов?

Anton Toshik 09.09.2018 11:33

@AntonToshik AFAIK, строка запроса так же видна, как и заголовок запроса, так что это скорее решение протокола, чем решение безопасности. используя HTTPS, строка запроса достаточно безопасна, я думаю

Daniel Leiszen 01.10.2018 20:16

@DanielLeiszen Как выглядит код подключения на стороне клиента сигнализации?

ScottFoster1000 21.12.2018 10:16

Спасибо, я видел много примеров, это единственное, что у меня сработало.

Hasni Mehdi 20.12.2019 06:24

Спасибо, Даниэль :-) Он работает с asp core 3.1 и IdentityServer4. Отличный вопрос и ответ.

slmglr 25.04.2020 19:48

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

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

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

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

Спасибо за обновления. В документе, на который вы ссылались, говорится: «При использовании WebSockets или событий, отправленных сервером, клиент браузера отправляет токен доступа в строке запроса. Получение токена доступа через строку запроса обычно так же безопасно, как и использование стандартного заголовка авторизации. Вы всегда должны использовать HTTPS. для обеспечения безопасного сквозного соединения между клиентом и сервером ». Об этом я тоже сказал в комментарии. При использовании аутентификации токена-носителя файлы cookie не используются.

Daniel Leiszen 07.09.2019 14:13

@DanielLeiszen да, но вы почти всегда храните свой токен на предъявителя в cookie, поэтому мы можем использовать cookie для получения токена при рукопожатии. Это обычная практика.

Eugene Chybisov 07.09.2019 15:45

вы имеете в виду, что сервер отправляет токен доступа / обновления, который затем сохраняется в файле cookie на стороне клиента и автоматически отправляется обратно во время запросов как часть пакета? Мне не известно о таком поведении, но я был бы рад увидеть несколько примеров. как создается cookie?

Daniel Leiszen 08.09.2019 17:56

@DanielLeiszen Например, у вас есть IdentityServer4 и клиентский SPA. Вы аутентифицируетесь на ID4 и перенаправляетесь на клиентский SPA с помощью access_token. (Я рекомендую использовать поток кода авторизации в качестве последнего предложения OAuth 2.0 для SPA) Затем в SPA вы сохраняете свой токен в файле cookie. И после этого пытаешься подключиться к signalR. Cookie будет автоматически отправляться на сервер каждый раз во время рукопожатия, а затем вы перехватываете их с помощью класса поиска и получаете токен. После этого у вас будет токен так же, как через строку запроса, но чуть более красивым способом.

Eugene Chybisov 09.09.2019 08:05

@DanielLeiszen Одна вещь, на которую следует обратить внимание - cookie всегда автоматически отправляет на сервер во время HTTP-запроса, вот как они работают. Для этого не нужно делать что-то дополнительно. Но вы, наверное, это уже знаете :)

Eugene Chybisov 09.09.2019 08:08

Файлы cookie сначала отправляются С сервера. Если сервер не настроен на отправку файлов cookie, отправлять обратно нечего. Использование только токенов на предъявителя не требует файлов cookie, поэтому отправлять обратно нечего.

Daniel Leiszen 17.09.2019 01:25

@DanielLeiszen изначально вы получаете токен не из cookie, который был получен с сервера, вы получаете токен из ответа конечной точки токена в формате json, и только после этого вы помещаете токен в cookie и сможете отправить их на сервер.

Eugene Chybisov 17.09.2019 10:27

Благодарю за разъяснение. Таким образом, cookie создается на стороне клиента и отправляется на сервер. Таким образом, в отличие от запроса со строкой aproach, файлы cookie могут не регистрироваться переходами во время связи. Однако без HTTPS это так же уязвимо, поскольку содержит текстовое значение токена доступа. На HTTPS этот подход безопаснее, чем строка запроса. Я прав?

Daniel Leiszen 24.09.2019 17:47

@DanielLeiszen точно :) Я надеюсь, что в наши дни никто не рассматривает возможность использования HTTP без S, так что я думаю, это можно считать правильным подходом.

Eugene Chybisov 25.09.2019 11:42

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

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 счастлив!

Это должен быть правильный ответ. Мне также нужно было добавить атрибут Authorize в SignalR Hub.

Jim Culverwell 20.04.2020 13:03

В то время, когда я отвечал на этот вопрос, лучшего способа решить эту проблему не было. Тем временем, благодаря этому сообщению и проблеме с github, MS удалось решить реальную проблему, стоящую за этой проблемой. Я не знаю, какова фактическая политика, позволяющая обновлять ответ без изменения репутации. Какие-либо предложения?

Daniel Leiszen 25.04.2020 00:55

Кроме того, в вопросе указывается фактическая версия SignalR, которая является версией, доступной в то время. Я думаю, что SignalR 2.0 принес много решений в этой области.

Daniel Leiszen 25.04.2020 01:00

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

Alex Gemma 26.04.2020 02:03

@AlexGemma: Я тоже не знаю, какой протокол должен быть в этой ситуации. Достоверность информации во времени всегда была слабым местом Интернета. И я не вижу никаких особенностей в SO. Я не беспокоюсь о своей репутации, я думаю, что голосование за ответ может помочь. Как я вижу из комментариев, в некоторых ситуациях этот ответ не работает даже с aspnet.core 3.1, но мой исходный ответ все еще работает.

Daniel Leiszen 26.04.2020 21:18

Я не вижу примера, чтобы этот ответ не работал в ASP.NET Core 3.1. В вашем вопросе ваш исходный код на самом деле был очень близок к этому ответу. Основное различие, которое я вижу, заключается в следующем: options.JwtBearerEvents = new JwtBearerEvents {OnMessagedReceived ... vs options.JwtBearerEvents.OnMessageReceived ... Я предлагаю отредактировать, чтобы включить этот параметр в выбранный ответ, но в том случае, если он не принимается, этот ответ все еще может быть здесь, как еще один вариант. Я ценю сотрудничество и голосование "за"!

Alex Gemma 27.04.2020 22:34

fyi, не передавайте префикс Bearer с токеном

MIKE 24.07.2020 20:13

@AlexGemma какую версию сигнализатора вы используете? Я использую AspNetCore.SignalR.Core 1.1.0, но у меня это все еще не сработало. Я должен был следовать тому пути, который был предложен в этом посте. stackoverflow.com/a/55926126/6012566

Amir 12.09.2020 16:59

@Amir Это ASP.NET Core 3.1. Да, вам также понадобится UseAuthentication ().

Alex Gemma 24.11.2020 19:53

Другие вопросы по теме