Reputation: 2773
I have an application that authenticates using OpenId. The code in the startup is:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = new PathString("/account/login");
})
.AddOpenIdConnect("CustomScheme", options =>
{
// options configured
});
I have a hub and wire up the hub in the app.UseEndpoints
as follows:
endpoints.MapHub<SpecialMessageHub>("myspecialhub");
Outside of posting my entire Startup.cs file, here is a quick overview:
app.UseAuthentication();
and app.UseAuthorization();
are before the app.UseEndpoints
services.AddSignalR();
and services.AddServerSideBlazor();
come after the services.AddAuthentication
and services.AddAuthorization
I've simplified my Hub for this question:
using Example.Extensions;
using Example.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading;
using System.Threading.Tasks;
namespace Example.Hubs
{
[Authorize]
public class SpecialMessageHub : Hub
{
public override Task OnConnectedAsync()
{
var customerId = Context.User.GetCustomerId();
Groups.AddToGroupAsync(Context.ConnectionId, GetCustomerGroupName(customerId));
return base.OnConnectedAsync();
}
public Task SendMessage(string customerId,
SpecialMessage message,
CancellationToken cancellationToken)
{
return Clients.Groups(GetCustomerGroupName(customerId)).SendAsync("ReceiveMessage", message, cancellationToken);
}
public static string GetCustomerGroupName(string customerId) => $"customers-{customerId}";
}
}
**Note: GetCustomerId
is an extension method in my project
I have a simple component just to test this:
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@using Example.Models
@inject Microsoft.AspNetCore.Components.NavigationManager NavigationManager
@implements IAsyncDisposable
<h2>@myMessage.Id</h2>
<div>@myMessage.Message</div>
@code {
private HubConnection hubConnection;
private SpecialMessage myMessage = new SpecialMessage();
protected override async Task OnInitializedAsync()
{
if (!IsConnected)
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/myspecialhub"))
.WithAutomaticReconnect()
.Build();
hubConnection.On<string, SpecialMessage>
("ReceiveMessage", (user, message) =>
{
myMessage = message;
StateHasChanged();
});
await hubConnection.StartAsync().ConfigureAwait(false);
}
}
public bool IsConnected => hubConnection != null
&& hubConnection.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
await hubConnection?.DisposeAsync();
}
}
I then add that component into my view with the below:
<component type="typeof(SpecialMessageComponent)" render-mode="ServerPrerendered"/>
When I hit this view, it returns a 401:
Based on this https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1, the cookie should be passed to the hub:
In a browser-based app, cookie authentication allows your existing user credentials to automatically flow to SignalR connections. When using the browser client, no additional configuration is needed. If the user is logged in to your app, the SignalR connection automatically inherits this authentication.
I am able to verify the user is authenticated and has the claim I am looking for both in the controller and in the component. I feel like this should be fairly straightforward and wouldn't require multiple hops, so any help would be greatly appreciated.
Here are some of the things I have tried:
Pull both the "access_token" and "id_token" from the Context within the View and pass it to the component as a parameter. Then based on here https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1#bearer-token-authentication, I did something like this, but this still resulted in a 401:
[Parameter]
public string AccessToken { get; set; }
protected override async Task OnInitializedAsync()
{
if (!IsConnected)
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/myspecialhub"), options =>
{
options.AccessTokenProvider = () => Task.FromResult(AccessToken);
})
.WithAutomaticReconnect()
.Build();
hubConnection.On<string, SpecialMessage>
("ReceiveMessage", (user, message) =>
{
myMessage = message;
StateHasChanged();
});
await hubConnection.StartAsync().ConfigureAwait(false);
}
}
}
I tried to manipulate the User by replacing the ClaimsPrincipal
with a new IIdentity
with the AuthenticationType
set to CookieAuthenticationDefaults.AuthenticationScheme
. This resulted in a nasty infinite loop but after restarting the app, the Identity was swapped, however, I still received a 401.
I have tried other options as well, but I cannot seem to get this to work with the OpenId.
Upvotes: 2
Views: 447
Reputation: 2773
In summary, the Blazor component is running on the server, so the HubConnection being made is from the server to the server.
await hubConnection.StartAsync().ConfigureAwait(false);
This establishes the server to server connection, which is why the HubCallerContext.User is not the user signed into the web application.
Personally, this was not intuitive to me. Because this is running on the server, there is not socket established with the browser. In order to accomplish this, one would have to use the Blazor WebAssembly to run the code on the client's browser. I've fallen back to handling this with JavaScript.
Here is the issue where Microsoft answers why this is not working: https://github.com/dotnet/aspnetcore/issues/36421
Upvotes: 1