1
votes

I am trying to get token authentication in a .net core app to work with signalR. The idea is, that a web frontend initiates a SignalR connection to the backend. The javascript signalR client adds a token to the SignalR connector as follows:

  const options: IHttpConnectionOptions  = {
  accessTokenFactory : (): Promise<string> => {
    return new Promise<string>((resolve) => {
      this.log('providing token');
      resolve(this.login.getToken());
    });
  }
};

this.connection = new HubConnectionBuilder()
  .withUrl('/mqtthub', options)
  .configureLogging(LogLevel.Debug)
  .build();

So far this works fine, I actually see the token show up when the signalR connection is negotiated. Here is where I start to wonder though. I can get the SignalR endpoint to validate the token, but validated or not the connetion is established anyway. I would rather unauthenticated connections where rejected straight away. But that is of secondary importance..

When I override public OnConnectedAsync() in the SignalR hub I do not get an authenticated user. And calling a Method on the signalR connection (from within the web frontend) fails because I am using the attribute [Authorize("Bearer")] on my function in the hub. I have gone through tons of examples but the combination of SignalR and JWT Auth seems to be rather rare, even more so in a .Net Core 2.1 implementation.

My setup in Startup.cs looks like this:

    public void ConfigureServices(IServiceCollection services)
{

  services.AddSingleton<IUserData, UserData>();

  services.AddAuthentication(options =>
  {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  }).AddJwtBearer(jwtOpts => {
    jwtOpts.TokenValidationParameters = TokenValidator.DefaultValidationParameters;
    //jwtOpts.SecurityTokenValidators.Clear();
    //jwtOpts.SecurityTokenValidators.Add(new TokenValidator());
  });

  services.AddAuthorization(auth =>
  {
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
      .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
      .RequireAuthenticatedUser().Build());
  });


  services.AddMvc();
  services.AddSignalR();
}


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }
  else
  {
    app.UseExceptionHandler("/Home/Error");
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
      ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
    });
  }


  app.UseStatusCodePagesWithReExecute("/");
  app.UseDefaultFiles();
  app.UseStaticFiles();

  app.UseAuthentication();
  // app.UseCors("CorsPolicy");
  app.UseMvc();

  app.UseSignalR(routes => { routes.MapHub<MQTTHub>("/mqtthub"); });

}

I have been using a custom JWT validator, now I using the system again. There does not seem to be anything wrong with validation as I see it is successfull. I am wondering though if certain content is needed in the JWToken to allow to authorize a user, I saw one sample put a whole bunch of claims into the token. Mine has the following content:

private string GenerateToken(string userid, string username)
{

  _iat = ToEpoch(DateTime.Now);
  _exp = ToEpoch(DateTime.Now.AddMinutes(20));

  var payload = new JwtPayload
  {
    {"iat", _iat },
    {"iss", _issuer},
    {"exp", _exp},
    {"nbf", _iat},
    {"aud", "*"},
    {"sub", userid},
    {"name", username},
    {"topicMask", $"^/{userid}/.*" },
    {"scope", _issuer},
  };

  var jwtToken = new JwtSecurityToken(_jwtHeader, payload);
  return JwtHandler.WriteToken(jwtToken);
}

Unfortunately I am rather new to ASP.NET and totally confused by all those middlewares. If anyone could point out what I missed out on ..

EDIT: I have a bit better understanding of the problem now. SignalR first negotiates. Here the token comes in the header and Token Auth works. Then when it actually connects a websocket in the second request, it sends the token again, but this time in a URL query parameter. And this is the request that is not authenticated.. So the question is how do I get the system to gather the token from a query parameter and authenticate.

This also leads to the question, why I would want to see my tokens in the query parameter. For one thing it leads to them showing up in the log which is not exactly a security feature. Maybe I am better off doing plain websockets after all..

1
You can add a middleware before UseAuthentication which gets the token from query string and adds it to header. - Kahbazi
@Kahbazi - any suggestions, what middleware to use ? - Thomas
you can write a custom middleware which does that. - Kahbazi

1 Answers

0
votes

Here's what to do:

1) Add a new middleware class

2) In the middleware Invoke(), transfer the auth token from the query string to the request auth header:

    /// <summary>
    /// Analyzes an incoming HttpContext to determine if the auth token should be taken from the query string
    /// </summary>
    /// <param name="httpContext">The HttpContext containing the request with user identity claims to analzye</param>
    /// <returns>A task to execute the next middleware component in the HTTP request processing pipeline</returns>
    public async Task Invoke(HttpContext httpContext)
    {
        var accessToken = httpContext.Request.Query["access_token"];
        if (!string.IsNullOrEmpty(accessToken))
        {
            httpContext.Request.Headers["Authorization"] = $"Bearer {accessToken}";
        }

        await next.Invoke(httpContext);
    }

3) Invoke this middleware BEFORE the auth middleware (app.UseAuthentication() in Startup.cs)

This will result in the auth token being available for the authentication middleware to consume.