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

13
我有两个aspnet.core服务: 一个用于IdentityServer4,另一个用于Angular4+客户端使用的API。SignalR hub在API上运行。整个解决方案都在docker上运行,但这应该不要紧(见下文)。
我使用隐式授权流程,它工作得非常完美。NG应用程序重定向到IdentityServer的登录页面,在那里用户登录。然后浏览器将带有访问令牌的重定向返回到NG应用程序。然后使用该令牌调用API,并建立与SignalR的通信。我认为我已经阅读了所有可用的资料(见下面的来源)。
由于SignalR使用不支持标头的WebSockets,因此令牌应该在查询字符串中发送。然后在API侧,提取并设置请求的令牌,就像在标头中一样。然后验证令牌并授权用户。
API没有任何问题,用户被授权并且声明可以在API侧检索。所以既然SignalR不需要任何特殊配置,那么IdentityServer也应该没有问题。我是对的吗?
当我不在SignalR hub上使用[Authorized]属性时,握手成功。这就是为什么我认为我使用的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代码部分。

我收到的日志信息和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;
            };
        });

系统中没有其他错误或异常。我可以对应用程序进行调试,一切似乎都很好。

这些日志行的含义是什么? 如何在授权期间调试正在发生的事情?

编辑:我差点忘了提到,我认为问题出在身份验证方案上,所以我将每个方案都设置为我认为需要的方案。然而,可悲的是它没有起到帮助作用。

我有点无头绪,所以我感谢任何建议。谢谢。

信息来源:

将auth token传递给SignalR

使用IdentityServer保护SignalR

关于SignalR授权的Microsoft文档

另一个GitHub问题

对SignalR进行身份验证

Identity.Application未经身份验证

3个回答

21

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

首先,我需要对所发生的事情有一些了解,因此我将整个授权机制替换为我的自己的机制。我可以通过这段代码实现它。虽然这不是解决方案所必需的,但如果有人需要,这是操作方式。

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

在使用IdentityServerAuthenticationHandler并重写每个可能的方法的帮助下,我最终明白了OnMessageReceived事件是在检查令牌后执行的。因此,如果在调用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握手期间客户端放置它。

编辑:我使用以下客户端端(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 是连接所需的必需参数。


1
非常感谢你,Daniel :) 在看到你的解决方案之前,我一直在追寻同样的问题!!! - Nabil.A
1
@DanielLeiszen 通过Websockets传递令牌时是否存在安全问题? - Anton Toshik
1
据我所知,查询字符串和请求头一样可见,因此这更多是协议决策而不是安全决策。使用HTTPS,我认为查询字符串已经足够安全了。 - Daniel Leiszen
1
@DanielLeiszen 你的SignalR客户端连接代码是什么样子的? - ScottFoster1000
1
谢谢,我看了很多例子,这是唯一一个对我有效的。 - Hasni Mehdi
显示剩余6条评论

6

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

简单来说,在以下示例中使用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;
            }
        };
    });
}

这篇微软文档给了我一个提示:https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1。然而,在微软的示例中,调用了`options.Events`,因为它不是使用IdentityServerAuthentication示例。如果像在微软示例中使用`options.Events`一样使用`options.JwtBearerEvents`,IdentityServer4就能正常工作了!

1
这应该是正确的答案。我还需要在SignalR Hub中添加Authorize属性。 - Jim Culverwell
1
当我回答这个问题的时候,没有更好的解决方法。与此同时,由于这篇文章和相关的 Github 问题,微软设法解决了这个问题背后的实际问题。我不知道更新回答的实际政策是什么,而不会影响声望。有什么建议吗? - Daniel Leiszen
此外,问题还指出了SignalR的实际版本,即当时可用的版本。我认为SignalR 2.0在这个领域带来了很多解决方案。 - Daniel Leiszen
1
我明白你的意思,丹尼尔。这是我在这里的第一篇帖子,我并不想影响你的声誉。我只是想分享一个新的解决方案。也许我可以建议对你的答案进行编辑,加入这个可能的解决方案,以免影响你的声誉? - Alex Gemma
1
请注意,不要将Bearer前缀与令牌一起传递。 - MIKE
显示剩余4条评论

2
让我来谈一下我的看法。我认为我们大多数人都将令牌存储在cookie中,并且在WebSockets握手期间它们也会被发送到服务器,因此我建议从cookie中检索令牌。
要做到这一点,请在最后一个if语句下面添加以下内容:
if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
    token = cookieToken;
}

实际上,根据Microsoft文档,我们完全可以从查询字符串中删除检索,因为这并不是真正安全的,而且可能会被记录在某个地方。


1
感谢更新。您引用的文档中提到:“使用WebSockets或Server-Sent Events时,浏览器客户端会在查询字符串中发送访问令牌。通过查询字符串接收访问令牌通常与使用标准授权头部一样安全。您应该始终使用HTTPS来确保客户端和服务器之间的安全端到端连接。”这也是我在评论中所说的。使用Bearer令牌身份验证时,不涉及cookie。 - Daniel Leiszen
1
@DanielLeiszen 是的,但你几乎总是将你的令牌存储在cookie中,因此我们可以使用cookie在握手时检索令牌。这是常见做法。 - Eugene Chybisov
1
你的意思是服务器发送访问/刷新令牌,然后在客户端存储在cookie中,并作为请求的一部分自动发送回来吗?我不知道这种行为,但很乐意看到一些例子。那么这个cookie是如何创建的呢? - Daniel Leiszen
1
例如,您有IdentityServer4和客户端SPA。您在ID4上进行身份验证,并使用access_token重定向到客户端SPA。(我建议使用Authorization Code Flow作为最新的OAuth 2.0建议用于SPA)然后在SPA中将令牌存储在cookie中。之后,您尝试连接signalR。每次握手期间,Cookie都会自动发送到服务器,然后您可以使用检索类捕获它们并获取令牌。之后,您将以与通过查询字符串相同的方式拥有令牌,但以稍微更漂亮的方式呈现。 - Eugene Chybisov
1
@DanielLeiszen 注意一件事情 - cookie 总是在 HTTP 请求期间自动发送到服务器,这就是它们的工作方式。你不需要为此做任何额外的事情。但你可能已经知道了 :) - Eugene Chybisov
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接