1

please note - this only applies to the case of server-side Identity (i.e. IdentityServer4 creates tokens, not Angular)

Created brand new asp.net core 5 angular spa app form template:

dotnet new angular --auth Individual
npm i @microsoft/signalr

Modified Startup.cs

            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder => builder
                .WithOrigins("http://localhost:4200")
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
            });
            services.AddSignalR();

    . . .

            app.UseCors("CorsPolicy");

            app.UseAuthentication();
            app.UseIdentityServer();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                . . .
                endpoints.MapHub<NewsHub>("/newshub");
            });

Added Hub class

    [Authorize]
    public class NewsHub : Hub
    {
    }

Modified WeatherForecastController:

        private IHubContext<NewsHub> _hub;

        public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
        {
            _hub = hub;
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var timerManager = new TimerManager(() => 
                _hub.Clients.All.SendAsync("servermessage", DateTime.Now.Ticks.ToString()));

Modify fetch-data.component.ts

    constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
      http.get<WeatherForecast[]>(baseUrl + 'weatherforecast').subscribe(result => {

      this.forecasts = result;

      this.hub = new HubConnectionBuilder()
        .withUrl("/newshub")
        .build();

      this.hub.on("servermessage", (m: string) => { console.log(m); });

      this.hub.start()
        .then(() => console.log('MessageHub Connected'))
        .catch(err => console.log('MessageHub Connection Error: ' + err.toString()));

    }, error => console.error(error));
  }

Authorization of SignalR hub fails. Output window:

IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Information: Profile service returned the following claim types: sub preferred_username name
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[2021-08-01T15:43:11.337Z] Information: Normalizing '/newshub' to 'https://localhost:44306/newshub'.
Failed to load resource: the server responded with a status of 401 () [https://localhost:44306/newshub/negotiate?negotiateVersion=1]
[2021-08-01T15:43:11.347Z] Error: Failed to complete negotiation with the server: Error
[2021-08-01T15:43:11.347Z] Error: Failed to start the connection: Error
MessageHub Connection Error: Error

If I remove [Authorize] attribute - it works fine

EDIT to people saying that I use cookie but need bearer token. Its not true. When I tried to specify Cookie as Authorization scheme on the hub class I got this error:

System.InvalidOperationException: No authentication handler is registered for the scheme 'Cookies'.
The registered schemes are: Identity.Application, Identity.External, Identity.TwoFactorRememberMe, Identity.TwoFactorUserId, idsrv, idsrv.external, IdentityServerJwt, IdentityServerJwtBearer.
Boppity Bop
  • 9,613
  • 13
  • 72
  • 151

3 Answers3

2

There is an obvious solution to it. I think it what @Chaodeng and @Stilgar were talking about only I was blinded by reading too many blogs. Here is the exact code one can use after creating an asp.net core angular app with identity:

Client side:

import { AuthorizeService } from '../../api-authorization/authorize.service';

. . .

constructor(. . . , authsrv: AuthorizeService) {

  this.hub = new HubConnectionBuilder()
    .withUrl("/newshub", { accessTokenFactory: () => authsrv.getAccessToken().toPromise() })
    .build();

Server side:

[Authorize]
public class NewsHub : Hub
Boppity Bop
  • 9,613
  • 13
  • 72
  • 151
  • This is definitely not the best place to ask this, but in case you know, what would this look like for a Blazor Webassembly app? I've tried multiple things, and this question is the closest thing I've found. – Peter Mghendi Feb 16 '22 at 12:41
  • @PeterLenjo there is github repo where you might try to ask it. And either way you should ask it here on SO anyway - just make sure you explain in details how it's not working. I personally have no idea as I never used blazor.. – Boppity Bop Feb 17 '22 at 18:22
  • Thank you, I followed that and ended up finding WPF sample that helped me out. – Peter Mghendi Feb 19 '22 at 04:25
1

If you want to use the signal client to pass the token to the backend hub, you should provide an access token instead of using a cookie. The server verifies the token and uses it to identify the user. This verification is only performed when the connection is established. During the lifetime of the connection, the server will not automatically re-authenticate to check for token revocation.

For details, you can read the official Microsoft documents:

https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#identity-server-jwt-authentication

Tupac
  • 2,590
  • 2
  • 6
  • 19
  • yes thank you.. but.. I already have had exactly same answer twice before.. please read my response here: https://learn.microsoft.com/en-us/answers/questions/497227/signalr-authorization-not-working-out-of-the-box-i.html?childToView=498268#answer-498268 - **I do not know how to access the token**, and also same document promises that SignalR must automatically authorize using cookie (again - see that link) – Boppity Bop Aug 02 '21 at 10:50
  • P.S. `If you want to use the signal client to pass the token` - i simply want signalr to be authorized. how - you tell me :) – Boppity Bop Aug 02 '21 at 10:51
  • Why not add cookie auth? – Stilgar Aug 02 '21 at 22:03
  • because I dont know how – Boppity Bop Aug 03 '21 at 07:32
  • Well, you need to try things out. Agree about the downvotes. – Stilgar Aug 04 '21 at 15:02
1

After several hours trying to solve the auth, an aspnetcore developer led me to believe there is no simple/automatic way to implement signalr authorization with angular without manually reimplementing identity and by-passing all the Identity Server conveniences..

So I invented this workaround.

The security is provided by authorization on the controller which links Identity Server userId and SignalR connectionId.

Controller

    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
        {
            this.hub = hub;
            this.logger = logger;
        }

        [HttpGet]
        [Route("{connectionId}")]
        public IEnumerable<WeatherForecast> GetForSignalR(string connectionId)
        {
            SurrogateAuth(connectionId);

            // NB: in real app - send particular data to particular users (by connection)
            var timerManager = new TimerManager(() => hub.Clients.Client(NewsHub.Connected.Keys.First()).SendAsync("servermessage", DateTime.Now.Ticks.ToString()));

    . . .

        private void SurrogateAuth(string connectionId)
        {
            var userId = GetApiUserSimple(this.HttpContext);
            NewsHub.Connected[connectionId].UserId = userId;
        }
        public static string GetApiUserSimple(HttpContext httpContext)
        {
            System.Security.Claims.ClaimsPrincipal currentUser = httpContext.User;
            var userId = currentUser.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;

            return userId;
        }

Hub

    public class NewsHub : Hub
    {
        public static readonly SortedDictionary<string, HubAuthItem> Connected = new SortedDictionary<string, HubAuthItem>();

        public override Task OnConnectedAsync()
        {
            NewsHub.Connected.Add(Context.ConnectionId, new HubAuthItem { ConnectionId = Context.ConnectionId, LastConnect = DateTime.Now });
            return base.OnConnectedAsync();
        }
        public override Task OnDisconnectedAsync(Exception exception)
        {
            Connected.Remove(Context.ConnectionId);
            return base.OnDisconnectedAsync(exception);
        }
    }

Controller

  constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {

    this.hub = new HubConnectionBuilder()
      .withUrl("/newshub")
      .build();

    this.hub.on("servermessage", (m: string) => { console.log(m); });

    this.hub.start()
      .then(() => {
        console.log(`MessageHub Connected: ${this.hub.connectionId}`);
        http.get<WeatherForecast[]>(baseUrl + 'weatherforecast/' + this.hub.connectionId).subscribe(result => {

          this.forecasts = result;

        }, error => console.log('Weather get error: ' + stringify(error)));

      })
      .catch(err => console.log('MessageHub connection error: ' + stringify(err)));
  }
Boppity Bop
  • 9,613
  • 13
  • 72
  • 151