Aspnetcore: Blazor (WASM) SignalR AccessTokenProvider issue (3.2.0-preview1)

Created on 30 Jan 2020  ·  20Comments  ·  Source: dotnet/aspnetcore

I have recently updated my Blazor project to latest preview version (3.2.0-preview1) and I am implementing the newly provided SignalR functionality for Client Side Blazor apps.

However, I have ran into an issue when using SignalR with auth.

Following this documentation (or this) I have configured my SignalR Client to use the AccessTokenProvider.
I would expect this to send the access_token as a query param (since the web-sockets do not support Headers), but this is not happening.


For anyone else who might read this and want a temporary work-around, thanks to @szmalec from the Blazor gitter chat. You can do something like the following:
.WithUrl(NavigationManager.ToAbsoluteUri("/chatHub?access_token=XXXX"))

But please be aware, this should only be temporary until the issue is fixed, as this method does not support WithAutomaticReconnect().

Blazor ♥ SignalR area-signalr bug

All 20 comments

@BrennanConroy ahh, yes it is.
@anurse was who recommend I raise the issue (on the aspnet blog post), but I see he beat me to it!

Oops! Let's dupe mine to yours since I did in fact ask you to file it 😝.

From my issue:

We can use the following code to detect Blazor WASM:

var isWasm = RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"));

Detecting wasm like this might change in the future https://github.com/mono/mono/issues/18627

I think we should look at including SignalR Client in the "3.2" release as part of this so we don't have to patch it.

Added to our project for tracking the progress

Just chatted with @BrennanConroy about this, here's my understanding:

  • The javascript client already has different behavior in WebSocketTransport - it accounts for not being able to use request headers while running in the browser, and so appends the accessToken to the URL as query string:

https://github.com/dotnet/aspnetcore/blob/0c3c6d8c9ba44bd90fca91d5231bd317e68d5554/src/SignalR/clients/ts/signalr/src/WebSocketTransport.ts#L34-L43

  • Whereas the javascript LongPollingTransport inserts the token into the header for the initial request here:

https://github.com/dotnet/aspnetcore/blob/0c3c6d8c9ba44bd90fca91d5231bd317e68d5554/src/SignalR/clients/ts/signalr/src/LongPollingTransport.ts#L78-L79

  • And updates the header for each subsequent poll here:

https://github.com/dotnet/aspnetcore/blob/0c3c6d8c9ba44bd90fca91d5231bd317e68d5554/src/SignalR/clients/ts/signalr/src/LongPollingTransport.ts#L127-L128

  • For the CSharp client, WebSocketTransport wrongly assumes it'll never run in the browser, and so always uses a request header:

https://github.com/dotnet/aspnetcore/blob/0c3c6d8c9ba44bd90fca91d5231bd317e68d5554/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs#L119-L124

The solution then would seem to be detecting whether or not we're running WASM (as @anurse described in https://github.com/dotnet/aspnetcore/issues/18697#issuecomment-580481058), and use a query string instead if we are running in WASM.

Yep, sounds right to me. One additional thing to consider is testing. We don't run tests in Blazor WASM and doing so is out-of-scope for this issue, but we should try to have unit tests. So we may need an internal property somewhere we can toggle from the test to force it to do the right thing so we can verify that the query string is set in a test (probably in https://github.com/dotnet/aspnetcore/blob/0c3c6d8c9ba44bd90fca91d5231bd317e68d5554/src/SignalR/server/SignalR/test/WebSocketsTransportTests.cs)

To add to this, if you're using Azure SignalR Service you will get a seperate access token from your hub when initializing the connection, which you will need to use to connect with Azure SignalR Service.

This also means the proposed work-around for placing the access_token in the URL will not work because it is a different token.

I hope this scenario will also be taken into account when addressing this issue.

@IIStevowII Yep, that's something the client already handles, it just currently tries to use a header instead of the URL. Our current fix works in the Azure SignalR scenario as well. Thanks for the heads-up though!

I tried the workaround:
.WithUrl(NavigationManager.ToAbsoluteUri("/chatHub?access_token=XXXX"))

But that doesn't work for me. I still get a 401 for Bearer header

Are you aware of the server changes needed to read the access_token?
https://docs.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1#bearer-token-authentication

I do this in my startup routine and that seems to work, because a cookie will be issues that is used for authentication in the SignalR hub and also my authorized controllers that provide images.

Do you see any issues with this solution?

    services.AddDefaultIdentity<ApplicationUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

    services.AddAuthentication()
        .AddIdentityServerJwt()
        .AddCookie();

    services.Configure<JwtBearerOptions>(
        IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
        options =>
        {
            var onTokenValidated = options.Events.OnTokenValidated;

            options.Events.OnTokenValidated = async context =>
            {
                if (onTokenValidated != null)
                {
                    await onTokenValidated(context);
                }

                var clientId = context.Principal.FindFirstValue("client_id");
                if (clientId == "temp.Client" && context.Principal.IsAuthenticated())
                {
                    await context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                        context.Principal);
                }
            };
        });

    // Add Cookies policy for SignalR and authenticated image controllers
    services.AddAuthorization(options => options.DefaultPolicy =
        new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme, IdentityServerJwtConstants.IdentityServerJwtBearerScheme)
            .RequireAuthenticatedUser()
            .Build());

    services.Configure<IdentityOptions>(options =>
        options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

    services.AddSignalR();
    services.AddControllers();

I found out that I actually don't have to issue an idditional cookie. There is already an Application authentication cookie that I can use. So this is my startup code and then SignalR just works fine, because it uses the application cookie for authentication:

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));

        services.AddDefaultIdentity<ApplicationUser>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

        services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

        services.AddAuthentication()
            .AddIdentityServerJwt();

        // Add Cookies policy for SignalR and <img> sources
        services.AddAuthorization(options => options.DefaultPolicy =
            new AuthorizationPolicyBuilder(IdentityConstants.ApplicationScheme, IdentityServerJwtConstants.IdentityServerJwtBearerScheme)
                .RequireAuthenticatedUser()
                .Build());

        services.Configure<IdentityOptions>(options =>
            options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

        services.AddSignalR();
        services.AddControllers();

If you're only running in the browser, then yes, the standard auth cookie should work fine. This API is designed for scenarios where you aren't in a browser or are calling a separate API (that requires a bearer token)

I have a mixed scenario, with a Blazor WASM web client (browser) and also a client that connects through the api (unity)

Right, so in the browser you don't need to configure the access token provider if you have cookie authentication already configured. In the Unity API you will need to use it. You may need to detect the presence of the browser if you have a shared code base (see our code which does this for an example)

Passing the access token in the querystring does not seem to work with AzureSignalR (Http 401).
Previously, I used js interop with the same connection info that I get from an Azure Function, so I know that my access token is ok.

@23min unfortunately the workaround described in the original post won't work, but we just checked in a fix that is queued for release in the next .NET Core 3.1 update that should work. The Azure SignalR negotiation process involves exchanging the access token you provide for a new one and that new one doesn't end up in the query string. With the proper fix, the .NET Client should work fine in Blazor WASM using Azure SignalR.

Closing this as the fix was merged in https://github.com/dotnet/aspnetcore/pull/20466 . Look for the release of ASP.NET Core 3.1.4 (we expect to ship it in early May).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

markrendle picture markrendle  ·  3Comments

farhadibehnam picture farhadibehnam  ·  3Comments

ipinak picture ipinak  ·  3Comments

fayezmm picture fayezmm  ·  3Comments

FourLeafClover picture FourLeafClover  ·  3Comments