SignalR AccessTokenProvider works with TypeScript client but not .NET client
Asked Answered
H

2

5

I'm attempting to pass an access_token through a HubConnection in a C# .NET Client. However, the result is not consistent with what I'm seeing through a TypeScript client. And this inconsistency results in a failed Authorization in the C# .NET Client but a successful authorization in the TypeScript client.

Here is the relevant code:

TypeScript

var builder = new signalr.HubConnectionBuilder();
builder.withUrl(hubUrl, {accessTokenFactory: () => token});

C#

var builder = new HubConnectionBuilder();
builder.WithUrl(url, o => {
  o.AccessTokenProvider = () => Task.FromResult(_token);
  //I've tried the following as well
  //o.Headers.Add("Authorization", "Bearer " + _token);
});

The TypeScript code generates an HTTP request as such:

POST http://localhost:5000/machine/negotiate?negotiateVersion=1 HTTP/1.1
Host: localhost:5000
Connection: keep-alive
Content-Length: 0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJleHAiOjE1NzYxODYwMTYsImlzcyI6Im5TY3J5cHQsIEluYy4iLCJhdWQiOiJuU3R1ZGlvIFVzZXIifQ.qxAzu-NgzlnfCqyysiML4Z0_s6UBTeRb7wcuGno9rk4
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) nstudio-pro/1.0.0 Chrome/78.0.3905.1 Electron/7.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://localhost:3000
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Referer: http://localhost:3000/main_window
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US
Cookie: .AspNetCore.Identity.Application=CfDJ8Dp0ZrsdLf1KpYnUb_iitp4gyw5TR5uJhBjiI8pFnzSSEM9ALjY6XbRVrMURJ_NrkK9IAD0Xlqy3l6HEQNJkfGG7vwCRfj5x4NjEW4Msm5GoQMuhG6epa3Q3r8QDhdC4z3tJSS0bRZ_EXvmnnnVWYvw4lILddLABWnf3leeMXrKUmq6AUAJPy1SVgj4fJQ8BGOo5HLPZDLZxN-m3ZV0jUaDkOf_mosTAz7JTjI53bAlxD0hi78YYzkVfpa8dEs8gXOTD85f96_m5DGGoMMCnvsjMP6ST1Q87rWHCCsxUPPLaH_A2xc6JpUqvzV-Pur6KtE8oFmcen4jq7h0kL2akXWUvTApZIxY7lFvFx7x4-8andT1DP7T3tRNdWnoRNRotSoQCp4HtS3Cz0GRwcyaKyhuFBjdUFMj1H0FKDYOEJEiarVMX0bElqgTjGGr7ZiOPyTJq1yHmCOraqdbP7YMycTWfC4F1tPXS0v4KxxNo8F2o31MYlhCx_sTIgEjJUHjdh9iugr401GYzazV3reL4M64YAliZ3fynzXf7ZNVqwUg-OvDzXd0nba4E3BVd_hQDwlssaWYq0DAZvrwO56iUwv1y9e-wehaH6OzocmvujVLX_HYG20BsXN6YdLiPPfhqNdkay50AaTuvXF2kq-exJTEnYBuc9U6eTWn9--mEFXKe4VQlTnn97AGvLfnOt_QrhUK4Pc88Z9q3hHNu7MNhfnE

Whereas the C# code creates:

POST http://localhost:5000/machine/negotiate?negotiateVersion=1 HTTP/1.1
Host: localhost:5000
User-Agent: Microsoft.AspNetCore.Http.Connections.Client/3.1.0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJleHAiOjE1NzYxODYxOTIsImlzcyI6Im5TY3J5cHQsIEluYy4iLCJhdWQiOiJuU3R1ZGlvIFVzZXIifQ.i_m-hnyZfPmFoUSX9VHPjSk-LP7UtpJlFafEuJBR66Q
X-Requested-With: XMLHttpRequest
Content-Length: 0

Then, the next requests are as such:

TypeScript

GET http://localhost:5000/machine?id=n2xReT4vy3KPuakBsaSBuA&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJleHAiOjE1NzYxODYwMTYsImlzcyI6Im5TY3J5cHQsIEluYy4iLCJhdWQiOiJuU3R1ZGlvIFVzZXIifQ.qxAzu-NgzlnfCqyysiML4Z0_s6UBTeRb7wcuGno9rk4 HTTP/1.1
Host: localhost:5000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) nstudio-pro/1.0.0 Chrome/78.0.3905.1 Electron/7.0.0 Safari/537.36
Upgrade: websocket
Origin: http://localhost:3000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US
Cookie: .AspNetCore.Identity.Application=CfDJ8Dp0ZrsdLf1KpYnUb_iitp4gyw5TR5uJhBjiI8pFnzSSEM9ALjY6XbRVrMURJ_NrkK9IAD0Xlqy3l6HEQNJkfGG7vwCRfj5x4NjEW4Msm5GoQMuhG6epa3Q3r8QDhdC4z3tJSS0bRZ_EXvmnnnVWYvw4lILddLABWnf3leeMXrKUmq6AUAJPy1SVgj4fJQ8BGOo5HLPZDLZxN-m3ZV0jUaDkOf_mosTAz7JTjI53bAlxD0hi78YYzkVfpa8dEs8gXOTD85f96_m5DGGoMMCnvsjMP6ST1Q87rWHCCsxUPPLaH_A2xc6JpUqvzV-Pur6KtE8oFmcen4jq7h0kL2akXWUvTApZIxY7lFvFx7x4-8andT1DP7T3tRNdWnoRNRotSoQCp4HtS3Cz0GRwcyaKyhuFBjdUFMj1H0FKDYOEJEiarVMX0bElqgTjGGr7ZiOPyTJq1yHmCOraqdbP7YMycTWfC4F1tPXS0v4KxxNo8F2o31MYlhCx_sTIgEjJUHjdh9iugr401GYzazV3reL4M64YAliZ3fynzXf7ZNVqwUg-OvDzXd0nba4E3BVd_hQDwlssaWYq0DAZvrwO56iUwv1y9e-wehaH6OzocmvujVLX_HYG20BsXN6YdLiPPfhqNdkay50AaTuvXF2kq-exJTEnYBuc9U6eTWn9--mEFXKe4VQlTnn97AGvLfnOt_QrhUK4Pc88Z9q3hHNu7MNhfnE
Sec-WebSocket-Key: 7XIeUeqljhqwC4AencqJxg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

C#

GET http://localhost:5000/machine?id=ZB_6NJvMFDpt0cRhoqiWkw HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJleHAiOjE1NzYxODYxOTIsImlzcyI6Im5TY3J5cHQsIEluYy4iLCJhdWQiOiJuU3R1ZGlvIFVzZXIifQ.i_m-hnyZfPmFoUSX9VHPjSk-LP7UtpJlFafEuJBR66Q
X-Requested-With: XMLHttpRequest
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: yUAfoo7mcEy8e/j4irLl5w==

My Startup.cs on the Server looks like:

services.AddAuthentication(jwtAuthScheme)
                .AddJwtBearer(jwtAuthScheme, options =>
                {
                    options.Events = new JwtBearerEvents
                    {
                        OnMessageReceived = context =>
                        {
                            if (context.Request.Query.ContainsKey("access_token"))
                            {
                                context.Token = context.Request.Query["access_token"];
                            }
                            else if (context.Request.Headers.TryGetValue("Authorization", out var value) && value.Count > 0)
                            {
                                context.Token = value[0].Substring("Bearer ".Length);
                            }
                            return Task.CompletedTask;
                        }
                    };
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        LifetimeValidator = (before, expires, token, param) =>
                        {
                            return expires > DateTime.UtcNow;
                        },
                        ValidateAudience = true,
                        ValidAudience = jwtHandler.TokenAudience,
                        ValidateIssuer = true,
                        ValidIssuer = jwtHandler.TokenIssuer,
                        ValidateActor = false,
                        ValidateLifetime = true,
                        IssuerSigningKey = jwtKey,
                        SaveSigninToken = true
                    };
                    options.SaveToken = true;
                    options.Audience = jwtHandler.TokenAudience;
                });
            services.AddAuthorization();

Is there anything that I'm doing wrong? Why does the Server fail to grab the token from the header and why does the C# .NET Client not place the token in the query string?

Hua answered 12/12, 2019 at 21:38 Comment(1)
Have you solved the problem?Spore
H
6

I have the same problem (the only difference from your case is that I'am not using JwtBearer, tokens are processed on the hub's side by itself). C# client is not able to authenticate on the SignalR hub (Asp.Net Core 3.1, Linux x64), while Javascript client does it easily.

C# client:

connection = new HubConnectionBuilder()
    .WithUrl("https://xxx/signalr", options =>
    { 
         options.AccessTokenProvider = () => Task.FromResult("abcdefgh");
    })
    .WithAutomaticReconnect()
    .Build();
 connection.StartAsync();

Javascript client:

 var connection = new signalR.HubConnectionBuilder()
       .withUrl('https://xxx/signalr', { accessTokenFactory: () => 'abcdefgh' })
       .withAutomaticReconnect()
       .build();
 connection.start().then(function () {...

I found that in the 1st case the request.query on the hub's side looks like this: wss://xxx/signalr?id=XVJmpqCyqtV76jLp1M9ew (no access_token parameter passed).

In 2nd case it looks normal (access_token parameter present): wss://xxx/signalr?id=XVJmpqCyqtV76jLp1M9ew&access_token=abcdefgh

UPDATE

Well, it's clear for me now. For not web-based clients (like C# client) token is always sent via Authorization bearer header; For web-based clients (javascript) token is sent via query string.

Ricky, in your case, I think, you don't need to extract token from Authorization header and assign it to context.Token manually - it's done automatically. So, your OnMessageReceived C# handler may look like this:

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
         if (context.Request.Query.ContainsKey("access_token"))
         {
              context.Token = context.Request.Query["access_token"];
         }
         return Task.CompletedTask;
    }
};
Hypothesis answered 17/12, 2019 at 10:14 Comment(0)
G
0

Its 2023 and still the issue persist with .Net desktop client. My workaround is to pass token in custom header in case token is not available in querystring.

Server/Hub side Auth :

OnMessageReceived = context =>
{
    var accessToken = context.Request.Query[SecurityKeys.AccessToken];
    if (string.IsNullOrEmpty(accessToken))
    {
        var authResult = context.Request.Headers.GetCommaSeparatedValues(SecurityKeys.Auth);
        accessToken = authResult[0];
    }

    // If the request is for our hub...
    var path = context.HttpContext.Request.Path;
    if (!string.IsNullOrEmpty(accessToken) &&
        (path.StartsWithSegments("/xyzhub/negotiate")))
    {
        // Read the token out of the query string
        context.Token = accessToken;
    }

    return Task.CompletedTask;
}

.Net client/Console side code:

var token = "*********";

hubConnection = new HubConnectionBuilder()
        .WithUrl(url, options =>
        {
            options.UseDefaultCredentials = true;
            options.AccessTokenProvider = () => Task.FromResult(token);
            options.Headers.Add("Auth", token);
        })
        .Build();
Glandulous answered 29/11, 2023 at 5:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.