AspNet.Core,IdentityServer 4:使用JWT承载令牌与SignalR 1.0进行websocket握手期间未授权(401)

时间:2018-06-01 09:31:09

标签: c# asp.net-core websocket identityserver4 asp.net-core-signalr

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

我使用隐式身份验证流程,它可以完美运行。 NG应用程序重定向到用户登录的IdentityServer的登录页面。之后,浏览器将被重定向回带有访问令牌的NG应用程序。然后,令牌用于调用API并与SignalR建立通信。我想我已经阅读了所有可用的内容(参见下面的资料)。

由于SignalR使用不支持头的websockets,因此令牌应该在查询字符串中发送。然后在API端,提取令牌并为请求设置令牌,就像它在标题中一样。然后验证令牌并授权用户。

API在没有任何问题的情况下工作,用户获得授权,并且可以在API端检索声明。因此,IdentityServer应该没有问题,因为SignalR不需要任何特殊配置。我是对的吗?

当我不使用SignalR集线器上的[授权]属性时,握手成功。这就是为什么我认为我使用的docker基础结构和反向代理没有任何问题(代理设置为启用websockets)。

因此,未经授权,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;
            };
        });

系统中没有其他错误,例外。我可以调试应用程序,一切似乎都没问题。

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

编辑:我差点忘了提到,我认为问题出在验证方案上,所以我将每个方案设置为我认为需要的方案。但遗憾的是它没有帮助。

我在这里很无能为力,所以我很感激任何建议。感谢。

信息来源:

Pass auth token to SignalR

Securing SignalR with IdentityServer

Microsoft docs on SignalR authorization

Another GitHub question

Authenticate against SignalR

Identity.Application was not authenticated

3 个答案:

答案 0 :(得分:12)

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

首先,我需要了解发生了什么,所以我将整个授权机制替换为我自己的。我可以通过这段代码来做到这一点。解决方案不需要它,但是如果有人需要它,这是可行的方法。

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

IdentityServerAuthenticationHandler的帮助下并覆盖所有可能的方法,我终于明白在检查令牌后执行OnMessageRecieved事件。因此,如果在调用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;
    }

这是我的自定义标记检索算法,它首先尝试标准标题和查询字符串,以支持Web API调用等常见情况。但是如果令牌仍然是空的,它会尝试从客户端在websocket握手期间放置它的查询字符串中获取它。

答案 1 :(得分:3)

我知道这是一个旧线程,但是如果有人像我一样偶然发现了这个线程。我找到了替代解决方案。

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文档给了我一个提示: https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1。但是,在Microsoft示例中,将调用options.Events,因为它不是使用IdentityServerAuthentication的示例。如果在Microsoft示例中使用options.JwtBearerEvents和options.Events的方式相同,则IdentityServer4很高兴!

答案 2 :(得分:2)

让我为此付两分钱。我认为我们大多数人都将令牌存储在cookie中,并且在WebSockets握手期间,令牌也将发送到服务器,因此我建议使用从cookie中检索令牌。

为此,请将其添加到最后一个if语句下面:

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

实际上,我们完全可以根据Microsoft docs从查询字符串中删除检索,这并不是真正安全的方法,可以记录在某处。