9

I made an asp.net core 2.0 SignalR Hub which uses Bearer Token for Authentication. Now I'm a bit lost on how to connect to it via the SignalR Angular 5 client. I actually can connect if I remove authorization from the Hub, so the connection is working, now I believe I just need to add the Authorization Bearer to the Http Headers of the connection.

The SignalR client reference in the package.json file of my Angular 5 project: "@aspnet/signalr-client": "^1.0.0-alpha2-final"

My Angular component:

import { Component, OnInit } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';
import { AuthenticationService } from '../core/authentication/authentication.service';
import { HubConnection } from '@aspnet/signalr-client';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  quote: string;
  isLoading: boolean;
  jwtToken:string;
  private hubConnection: HubConnection;

  constructor(
    private _http: HttpClient, 
    private _auth : AuthenticationService,
    private _toastr: ToastrService) { }

  ngOnInit() {
    this.isLoading = false;
    this.jwtToken = this._auth.currentToken;

    this.hubConnection = new HubConnection('http://localhost:27081/hub/notification/');
    this.hubConnection
      .start()
      .then(() => console.log('Connection started!'))
     .catch(err => console.error('Error while establishing connection :(', err));        
    this.hubConnection.on("send", data => {
        console.log(data);
    });
  }

  showToastr(){
    this._toastr.success('Hello world!', 'Toastr fun!');
  }

}

Due to reading similar questions I tried: this.hubConnection.Headers.Add("token", tokenValue); but it doesn't work, the Headers property doesn't exist.

How can I add the Bearer token to the Http Headers of the HubConnection?

Thanks for any help

André Luiz
  • 4,670
  • 6
  • 41
  • 76

3 Answers3

15

To do this with @aspnet/signalr (^1.1.4) you can use the following code

const options: IHttpConnectionOptions = {
  accessTokenFactory: () => {
    return "Token is resolved here";
  }
};

const connection = new signalR.HubConnectionBuilder()
  .configureLogging(signalR.LogLevel.Information)
  .withUrl(`${environment.apiUrl}/notify`, options)
  .build();

Also add an annotation to your Hub

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

As a side note, SignalR when using the websocket protocol does not seem to attach the Bearer token as a header and instead adds it to the request URL as an 'access_token' parameter, this requires you to configure your authentication to handle this token when signalR chooses to use ws.

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = false;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false
                };
                x.Events= 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("/notify")))
                        {
                            // Read the token out of the query string
                            context.Token = accessToken;
                        }
                        return Task.CompletedTask;
                    }
                };
            });
Darryn Hosking
  • 2,688
  • 2
  • 13
  • 20
  • 1
    This post was the only thing that worked for me, specifically the part where you set the bearer events options. I all of a sudden was having connection issues in Chrome only, but I hadn't touched my chat hub code. I can't begin to understand why this worked for me, but Thank you for sharing! – Eric Dec 06 '19 at 22:09
  • @Daxxy Could you elaborate here `Token is resolved here`? Where is the token coming from? – thodwris Mar 05 '20 at 01:25
  • 1
    @Daxxy also why we need this? `{ ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false };` And I get an error in this line `IssuerSigningKey = new SymmetricSecurityKey(key)` because of undefined `key` – thodwris Mar 05 '20 at 01:29
  • 2
    I want to achieve CORS, How to pass headers along with access token in IHttpConnectionOptions ? – Krunal Shah Jun 24 '20 at 05:43
  • @KrunalShah here's a reference to add CORS https://docs.microsoft.com/en-us/aspnet/core/signalr/security?view=aspnetcore-3.1 – jan4co Jul 24 '20 at 04:17
  • @thodwris The "Token resolved here" would be replaced with code that resolves your token, ie in my case i read the token that i had stored in local storage of the browser. – Darryn Hosking Aug 28 '20 at 10:00
  • 1
    @thodwris the key should be defined elsewhere, this section of code is part of the standard setup for JWT tokens and is inconsequential to the signalr issue. The only part that is actually needed for signalr would be the JwtBearerEvents section, the rest is all just there for the standard jwt setup and can vary based on how you set it up in your project. Keep in mind this is just short snippets of code with the intention of giving an idea of what needs to be done in order to implement this. – Darryn Hosking Aug 28 '20 at 10:10
  • Instead of adding OnMessageReceived which didnt work for me, adding a middleware to rewrite the access_token to the HTTP-Header did the job: https://github.com/abpframework/abp/issues/5239 – Kelon Apr 11 '21 at 17:48
5

From reading their source code and tests, it looks like you can provide an options object containing your access token, like so

var options = {
    transport: transportType,
    logging: signalR.LogLevel.Trace,
    accessToken: function () {
        return jwtToken;
    }
};

hubConnection = new signalR.HubConnection('/authorizedhub', options);
hubConnection.start();

The code in particular in the test file here

user184994
  • 15,680
  • 1
  • 33
  • 45
  • Thanks for your answer! I couldn't solve using your answer. I was reading and it looks like you can't set the headers. I managed to solve using this article: https://damienbod.com/2017/10/16/securing-an-angular-signalr-client-using-jwt-tokens-with-asp-net-core-and-identityserver4/ – André Luiz Jan 05 '18 at 19:21
  • This works Great!. In mine case I have to specify AuthenticationScheme in Authorize attibute i.e. [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] – Kamran Asim Jul 18 '19 at 05:44
1

I couldn't find a way to solve using angular but I did it using asp.net following this article.

This is what I did: Now to connect I pass the jwt token in the querystring and specify transport type:

const options = {
      transport: TransportType.WebSockets
};
this.hubConnection = new HubConnection('http://localhost:27081/hub/notification/?token='+this.jwtToken, options);

And then in the startup.cs > ConfigureServices() :

        services
            .AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = Configuration["JwtIssuer"],
                    ValidAudience = Configuration["JwtIssuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
                    ClockSkew = TimeSpan.Zero // remove delay of token when expire
                };
                cfg.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        if (context.Request.Query.TryGetValue("token", out StringValues token)
                        )
                        {
                            context.Token = token;
                        }

                        return Task.CompletedTask;
                    },
                    OnAuthenticationFailed = context =>
                    {
                        var te = context.Exception;
                        return Task.CompletedTask;
                    }
                };
            });
umutesen
  • 2,124
  • 1
  • 21
  • 37
André Luiz
  • 4,670
  • 6
  • 41
  • 76
  • I have updated my answer with how to do this correctly using angular. You should use the accessTokenFactory to resolve your token and pass it in as options. When doing this it will send the token as an "access_token" query parameter. – Darryn Hosking Dec 12 '19 at 06:00
  • @André Luiz the this.jwtToken is undefined for me – thodwris Mar 05 '20 at 01:33