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

I have to answer my own question because I had a deadline and surprisingly I managed to solve this one. So I write it down hoping it is going to help someone in the future.

First I needed to have some understanding what was happening, so I replaced the whole authorization mechanism to my own. I could do it by this code. It is not required for the solution, however if anyone needed it, this is the way to do.

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

With the help of IdentityServerAuthenticationHandler and overriding every possible method I finally understood that the OnMessageRecieved event is executed after the token is checked. So if there weren't any token during the call for HandleAuthenticateAsync the response would be 401. This helped me to figure out where to put my custom code.

I needed to implement my own "protocol" during token retrieval. So I replaced that mechanism.

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

The important part is the TokenRetriever property and what comes to replace it.

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

And this is my custom token retriever algorithm that tries the standard header and query string first to support the common situations such as web API calls. But if the token is still empty it tries to get it from the query string where client put it during websocket handshake.

EDIT: I use the following client side (TypeScript) code in order to provide the token for the SignalR handshake

import { HubConnection, HubConnectionBuilder, HubConnectionState } from '@aspnet/signalr';
// ...
const url = `${apiUrl}/${hubPath}?signalr_token=${accessToken}`;
const hubConnection = new HubConnectionBuilder().withUrl(url).build();
await hubConnection.start();

Where apiUrl, hubPath and accessToken are the required parameters of the connection.


I know this is an old thread, but in case someone stumbles upon this like I did. I found an alternative solution.

TLDR: JwtBearerEvents.OnMessageReceived, will catch the token before it is checked when used as illustrated below:

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

This Microsoft doc gave me a hint: https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1. However, in the Microsoft example, options.Events is called, because it is not an example using IdentityServerAuthentication. If options.JwtBearerEvents is used the same way as options.Events in the Microsoft example, IdentityServer4 is happy!


Let me put my two cents to this. I think most of us store tokens in cookies and during WebSockets handshake they are also sent to the server, so I suggest using token retrieval from cookie.

To do this add this below last if statement:

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

Actually we could delete retrieval from query string at all as according to Microsoft docs this is not truly secure and can be logged somewhere.