C# AspNet.Core,IdentityServer 4:在使用JWT承载令牌与信号器1.0进行websocket握手期间未经授权(401)

C# AspNet.Core,IdentityServer 4:在使用JWT承载令牌与信号器1.0进行websocket握手期间未经授权(401),c#,asp.net-core,websocket,identityserver4,asp.net-core-signalr,C#,Asp.net Core,Websocket,Identityserver4,Asp.net Core Signalr,我有两个aspnet.core服务。一个用于IdentityServer 4,另一个用于Angular4+客户端使用的API。信号集线器在API上运行。整个解决方案在docker上运行,但这并不重要(见下文) 我使用隐式auth流,它可以完美地工作。NG应用程序重定向到用户登录的IdentityServer的登录页面。之后,浏览器将被重定向回带有访问令牌的NG应用程序。然后,令牌用于调用API并建立与信号器的通信。我想我已经阅读了所有可用的资料(见下面的资料来源) 由于SignalR使用的Web

我有两个aspnet.core服务。一个用于IdentityServer 4,另一个用于Angular4+客户端使用的API。信号集线器在API上运行。整个解决方案在docker上运行,但这并不重要(见下文)

我使用隐式auth流,它可以完美地工作。NG应用程序重定向到用户登录的IdentityServer的登录页面。之后,浏览器将被重定向回带有访问令牌的NG应用程序。然后,令牌用于调用API并建立与信号器的通信。我想我已经阅读了所有可用的资料(见下面的资料来源)

由于SignalR使用的WebSocket不支持标头,因此应在querystring中发送令牌。然后在API端,提取令牌并为请求设置令牌,就像在头中一样。然后验证令牌并授权用户

API工作没有任何问题,用户获得授权,并且可以在API端检索声明。所以IdentityServer应该不会有问题,因为Signal不需要任何特殊配置。我说得对吗

当我不在信号集线器上使用[Authorized]属性时,握手成功。这就是为什么我认为docker基础设施和我使用的反向代理(代理设置为启用WebSocket)没有问题

所以,未经授权,信号机工作。通过授权,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;
            };
        });
系统中没有其他错误、异常。我可以调试应用程序,一切似乎都很好

包含的日志行是什么意思? 如何调试授权期间发生的事情

编辑:我差点忘了提到,我认为问题在于身份验证方案,因此,我将每个方案都设置为我认为需要的方案。不管多么可悲,这都没有帮助

我在这里有点不知所措,所以我很感激你的建议。谢谢

资料来源:


我必须回答我自己的问题,因为我有一个最后期限,令人惊讶的是,我设法解决了这个问题。所以我把它写下来,希望它能在将来帮助别人

首先,我需要了解发生了什么,所以我将整个授权机制替换为我自己的。我可以用这个密码。解决方案不需要它,但是如果有人需要它,这就是解决方法

services.Configure<AuthenticationOptions>(options =>
{
    var scheme = options.Schemes.SingleOrDefault(s => s.Name == JwtBearerDefaults.AuthenticationScheme);
    scheme.HandlerType = typeof(CustomAuthenticationHandler);
});
重要的部分是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;
    }

其中APIRL、hubPath和accessToken是连接所需的参数。

让我为此付出两分。我认为我们大多数人将令牌存储在cookie中,在WebSocket握手过程中,令牌也会被发送到服务器,因此我建议使用cookie中的令牌检索

要执行此操作,请在下面添加最后一条
if
语句:

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

事实上,我们可以从查询字符串中删除检索,因为这不是真正安全的,可以记录在某个地方。

我知道这是一个旧线程,但万一有人像我一样偶然发现了它。我找到了另一个解决办法

TLDR: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文档给了我一个提示:
. 但是,在Microsoft示例中,会调用options.Events,因为它不是使用IdentityServer身份验证的示例。如果options.JwtBearerEvents的使用方式与Microsoft示例中的options.Events相同,则IdentityServer4很高兴

五,。有趣。我面临着类似的问题。如何在客户端JS(或TS)中添加自定义头?您是否对信号机npm包进行了更改?我在api中没有看到任何可以传递头-thankshi-thanks的东西。我现在意识到您正在查询字符串中传递身份验证令牌,对吗?我已经在工作了。我以为你已经找到了一种使用授权头而不是查询字符串的方法,但我不认为你正在这么做?是的,浏览器似乎不允许设置头,尽管你可以从其他类型的客户端进行设置。不幸的是,我在浏览器中,所以需要使用查询字符串-再次感谢,多谢丹尼尔:)在看到你的解决方案之前,我一直在寻找相同的问题@DanielLeiszen是否存在与使用websockets通过查询传递令牌相关的安全问题?感谢更新。你提到的文件说使用WebSocket或服务器发送的事件时,浏览器客户端会在查询字符串中发送访问令牌。通过查询字符串接收访问令牌通常与使用标准授权头一样安全。您应该始终使用HTTPS来确保客户端和服务器之间的安全端到端连接。“这也是我在评论中所说的。使用承载令牌身份验证时,cookies不起作用。@DanielLeiszen是的,但您几乎总是将承载令牌存储在cookie中,因此我们可以使用cookie在握手时检索令牌。这是常见的做法。您的意思是服务器发送访问/刷新令牌,然后将其存储在客户端的cookie中,并在请求期间作为包的一部分自动发送回?我不知道有这种行为,但我很乐意看到一些例子。cookie是如何创建的?@DanielLeiszen例如,您有IdentityServer4和客户端SPA。您在ID4上进行身份验证,并使用access\u令牌重定向到客户端SPA。(我建议使用Aut
if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
    token = cookieToken;
}
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;
            }
        };
    });
}