简体   繁体   中英

Blazor WebAssembly SignalR Authentication

I would love to see an example on how to add authentication to a SignalR hub connection using the WebAssembly flavor of Blazor. My dotnet version is 3.1.300.

I can follow these steps to get an open, unauthenticated SignalR connection working: https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr-blazor-webassembly?view=aspnetcore-3.1&tabs=visual-studio

All the tutorials I find seem older or are for a server-hosted type, and don't use the built-in template.

I have added authentication to the rest of the back-end, using the appropriate template and these instructions, including the database: https://docs.microsoft.com/en-us/aspnet/core/security/blazor/?view=aspnetcore-3.1

But every time I add [Authenticate] to the chat hub, I get an error returned. Is there any way, extending the first tutorial, that we can authenticate the hub that is created there? It would be great to hitch on to the built-in ASP.NET system, but I am fine just passing a token in as an additional parameter and doing it myself, if that is best. In that case I would need to learn how to get the token out of the Blazor WebAssembly, and then look it up somewhere on the server. This seems wrong, but it would basically fill my needs, as an alternative.

There are all sorts of half-solutions out there, or designed for an older version, but nothing to build off the stock tutorial that MS presents.

Update: Following the hints in this news release https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-preview-2-release-now-available/ , I now can get a token from inside the razor page, and inject it into the header. I guess this is good?? But then how do I get it and make use of it on the server?

Here is a snippet of the razor code:

protected override async Task OnInitializedAsync()
{
    var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri(UriHelper.BaseUri);

    var tokenResult = await AuthenticationService.RequestAccessToken();

    if (tokenResult.TryGetToken(out var token))
    {
        httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");

        hubConnection = new HubConnectionBuilder()
            .WithUrl(UriHelper.ToAbsoluteUri("/chatHub"), options =>
            {
                options.AccessTokenProvider = () => Task.FromResult(token.Value);
            })
            .Build();
    }
}

Update 2: I tried the tip in here: https://github.com/dotnet/aspnetcore/issues/18697

And changed my code to:

        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chatHub?access_token=" + token.Value))
            .Build();

But no joy.

I've come across the same issue.

My solution was 2-sided: I had to fix something in the fronend and in the backend.

Blazor

In your connection builder you should add the AccessTokenProvider:

string accessToken = "eyYourToken";
connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:5001/hub/chat", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(token.Value);
    })
    .Build();

options.AccessTokenProvider is of type Func<Task<string>> , thus you can also perform async operations here. Should that be required.

Doing solely this, should allow SignalR to work.

Backend

However! You might still see an error when SignalR attempts to create a WebSocket connection. This is because you are likely using IdentityServer on the backend and this does not support Jwt tokens from query strings. Unfortunately SignalR attempts to authorize websocket requests by a query string parameter called access_token .

Add this code to your startup:

.AddJwtBearer("Bearer", options =>
{
    // other configurations omitted for brevity
    options.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("/hubs"))) // Ensure that this path is the same as yours!
            {
                // Read the token out of the query string
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});

edit 1 : Clarified the usage of the Blazor SignalR code

This is my solution and works

[Inject] HttpClient httpClient { get; set; }
[Inject] IAccessTokenProvider tokenProvider { get; set; }
HubConnection hubConnection { get; set; }

(...)

private async Task ConnectToNotificationHub()
{
    string url = httpClient.BaseAddress.ToString() + "notificationhub";

    var tokenResult = await tokenProvider.RequestAccessToken();

    if (tokenResult.TryGetToken(out var token))
    {
        hubConnection = new HubConnectionBuilder().WithUrl(url, options =>
        {
            options.Headers.Add("Authorization", $"Bearer {token.Value}");
        }).Build();


        await hubConnection.StartAsync();

        hubConnection.Closed += async (s) =>
        {
            await hubConnection.StartAsync();
        };

        hubConnection.On<string>("notification", m =>
        {
            string msg = m;
        });
    }
}

In my case (Blazor WebAssembly, hosted on ASP.NET Core 5.0 using JWT Bearer Token Auth), I had to add the following:

Blazor WASM Client

When building the connection (in my case: in the constructor of some service proxy class), use IAccessTokenProvider and configure the AccessTokenProvider option like so:

public ServiceProxy(HttpClient httpClient, IAccessTokenProvider tokenProvider) {
    HubConnection = new HubConnectionBuilder()
        .WithUrl(
            new Uri(httpClient.BaseAddress, "/hubs/service"),
            options => {
                options.AccessTokenProvider = async () => {
                    var result = await tokenProvider.RequestAccessToken();
                    if (result.TryGetToken(out var token)) {
                        return token.Value;
                    }
                    else {
                        return string.Empty;
                    }
                };
            })
        .WithAutomaticReconnect() // optional
        .Build();
}

ASP.NET Core Server

Add the following to Startup.ConfigureServices :

services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => {
    // store user's "name" claim in User.Identity.Name
    options.TokenValidationParameters.NameClaimType = "name";

    // pass JWT bearer token to SignalR connection context
    // (from https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0)
    options.Events = new JwtBearerEvents {
        OnMessageReceived = context => {
            var accessToken = context.Request.Query["access_token"];
            // If the request is for on of our SignalR hubs ...
            if (!string.IsNullOrEmpty(accessToken) &&
                (context.HttpContext.Request.Path.StartsWithSegments("/hubs/service"))) {
                 // Read the token out of the query string
                 context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM