簡體   English   中英

AspNet.Core、IdentityServer 4:在使用 JWT 不記名令牌與 SignalR 1.0 進行 websocket 握手期間未經授權 (401)

[英]AspNet.Core, IdentityServer 4: Unauthorized (401) during websocket handshake with SignalR 1.0 using JWT bearer token

我有兩個 aspnet.core 服務。 一種用於 IdentityServer 4,一種用於 Angular4+ 客戶端使用的 API。 SignalR 集線器在 API 上運行。 整個解決方案在 docker 上運行,但這應該無關緊要(見下文)。

我使用了完美無缺的隱式身份驗證流程。 NG 應用程序重定向到用戶登錄的 IdentityServer 的登錄頁面。之后,瀏覽器將使用訪問令牌重定向回 NG 應用程序。 然后使用令牌調用 API 並建立與 SignalR 的通信。 我想我已經閱讀了所有可用的內容(請參閱下面的來源)。

由於 SignalR 使用不支持標頭的 websocket,因此應在查詢字符串中發送令牌。 然后在 API 端,令牌被提取並設置為請求,就像它在標頭中一樣。 然后驗證令牌並授權用戶。

API 工作沒有任何問題,用戶獲得授權,並且可以在 API 端檢索聲明。 因此 IdentityServer 應該沒有問題,因為 SignalR 不需要任何特殊配置。 我對嗎?

當我不在 SignalR 集線器上使用 [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 中,我在其中獲取和提取令牌以及身份驗證配置。

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

使用 IdentityServer 保護 SignalR

關於 SignalR 授權的 Microsoft 文檔

另一個 GitHub 問題

針對 SignalR 進行身份驗證

Identity.Application 未通過身份驗證

我必須回答我自己的問題,因為我有一個截止日期,而且令人驚訝的是我設法解決了這個問題。 所以我把它寫下來,希望它會在未來幫助某人。

首先,我需要了解發生了什么,所以我將整個授權機制替換為我自己的。 我可以通過這段代碼做到這一點。 解決方案不需要它,但是如果有人需要它,這就是方法。

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 握手期間放置它的查詢字符串中獲取它。

編輯:我使用以下客戶端(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 是連接的必需參數。

我知道這是一個舊線程,但萬一有人像我一樣偶然發現了這個。 我找到了一個替代解決方案。

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;
            }
        };
    });
}

這個微軟文檔給了我一個提示: https : //docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz? view = aspnetcore- 3.1 但是,在 Microsoft 示例中,調用了 options.Events,因為它不是使用 IdentityServerAuthentication 的示例。 如果 options.JwtBearerEvents 的使用方式與 Microsoft 示例中的 options.Events 相同,那么 IdentityServer4 很高興!

讓我把我的兩分錢用於這個。 我認為我們大多數人都將令牌存儲在 cookie 中,並且在 WebSockets 握手期間,它們也會被發送到服務器,因此我建議使用從 cookie 中檢索令牌。

為此,在最后一個if語句中添加以下內容:

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

實際上,我們完全可以從查詢字符串中刪除檢索,因為根據Microsoft 文檔,這不是真正安全的,可以記錄在某處。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM