Search code examples
c#asp.net-coreazure-active-directorysignalrazure-ad-msal

SignalR and AzureAD authentication : loose websocket in favor of long polling


Our Asp.net core/React application used SignalR with good performance.
We added an authentication, made client side (React), with msal. Since I added the AzureAD authentication, the negotiation of SignalR connection between client and server goes from WebSockets -> ServerSideEvents -> Long polling
With Long polling the performance degrades enormously.

Connection phase

In connection phase, there is a 401, despite the Jwt being passed both as query string and header

Here is the client code, passing the token:

let optimizerConnection = new HubConnectionBuilder()
  .withUrl("/hubpath", 
            { 
            accessTokenFactory: () => getRawJwtToken() },
            false, // skipNegotiation
             HttpTransportType.WebSockets //transport
             )
  .withAutomaticReconnect()
  .configureLogging(LogLevel.Trace)
  .build()

Here is the the Hub with required authentication (the identity information, claims,...) goes to the server with the [Authorize] attribute. But authentication makes to use long polling, and is so slow:

[Authorize(AuthenticationSchemes=JwtBearerDefaults.AuthenticationScheme, Roles="Optimizer")]
public class SomeHub : Hub, ISomeHub { ...}

Here is the configuration of the identity server side:

var configuration = new ConfigurationBuilder()
      .AddJsonFile("appsettings.json")
      .Build();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(configuration);

I read all the posts I found about the subject, sometimes with Jwt authentication (not AzureAD). Some posts mention an OnMessageReceived method. I implemented OnMessageReceived, but it is not called:

  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
          .AddMicrosoftIdentityWebApi(ConfigureJwtBearerOptions,
                                      ConfigureMicrosoftIdentityOptions,
                                      jwtBearerScheme: "Bearer",
                                      subscribeToJwtBearerMiddlewareDiagnosticsEvents: true);

private static void ConfigureMicrosoftIdentityOptions(MicrosoftIdentityOptions options)
{
  options.Authority = "https://login.microsoftonline.com";
  options.ClientId = "myClientIdGuid";
  options.Instance = "https://login.microsoftonline.com/myInstanceGuid";
}
private static void ConfigureJwtBearerOptions(JwtBearerOptions options)
{
  options.Events = new JwtBearerEvents
  {
    OnChallenge = OnChallenge,
    OnTokenValidated = OnTokenValidated,
    OnAuthenticationFailed = OnAuthenticationFailed,
    OnForbidden = OnForbidden,
    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(SomeHub.Path)))
      {
        // Read the token out of the query string
        context.Token = accessToken;
      }
      return Task.CompletedTask;
    }
  };
}

If anybody has a clue, thank you


Solution

  • I got it working ! Even if I didn't get the OnMessageReceived to be called.

    A modification was adding a service - likely to be closely equivalent to AddAuthentication() + AddMicrosoftIdentityWeabApi()

    services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd");
    

    And then add a custom middleware before UseAuthentication()

      app.UseMiddleware<AccessTokenMiddleware>();
      app.UseAuthentication();
    

    The middleware puts the query string parameter in the headers

    public class AccessTokenMiddleware
    {
      private readonly RequestDelegate next;
      public AccessTokenMiddleware(RequestDelegate next)
      {
        this.next = next;
      }
      public async Task Invoke(HttpContext httpContext)
      {
        var request = httpContext.Request;
        // Web sockets cannot pass headers so the access token must be taken from query param and
        // added to the header before authentication middleware runs
        if (request.Path.StartsWithSegments(SomeHub.Path, StringComparison.OrdinalIgnoreCase) &&
            request.Query.TryGetValue("access_token", out var accessToken))
          request.Headers.Add("Authorization", $"Bearer {accessToken}");
        await next(httpContext);
      }
    }