dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.38k stars 10k forks source link

Blazor WebAssembly client authenticates With SignalR Hub but cannot send Client.User messages #23452

Closed fandango57 closed 4 years ago

fandango57 commented 4 years ago

I've created with Visual Studio a BlazorWebAssembly App with Authentication, ASP.NET Core hosted, PWA, selections.

In the Startup I have cookie policy for SignalR and the client authenticates with SignalR Hub and Context information can be obtained. Messages can be sent via Clients.Client(connectionId) or Clients.All but not to Client.User. Since user Identiy is authenticated and identity known I could create database or dictionary mapping email to connectionIds and send that way. No errors are thrown and authorization to hub is successful. I'm wondering what I am missing or with Blazor do you send only by connectionId and not via Client.User?

Below are Startup.cs, ChatHubTest.cs, ChatTest.Razor, Program.cs.

Thanks

Patrick

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddDefaultIdentity<ApplicationUser>(options =>
            options.SignIn.RequireConfirmedAccount = false)
                 .AddRoles<IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
            {
                options.IdentityResources["openid"].UserClaims.Add("name");
                options.ApiResources.Single().UserClaims.Add("name");
                options.IdentityResources["openid"].UserClaims.Add("role");
                options.ApiResources.Single().UserClaims.Add("role");
            });

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap
                .Remove("role");

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

            services.AddAuthorization(options =>
            {
                options.AddPolicy("TeamMember", policy => policy.RequireRole("TeamMember"));
                options.AddPolicy("IsMember", policy => policy.RequireClaim("IsMember", "true"));
            });

            services.AddAuthentication().AddIdentityServerJwt();
.
            services.AddAuthorization(options => options.DefaultPolicy =
                new AuthorizationPolicyBuilder(IdentityConstants.ApplicationScheme, 
                IdentityServerJwtConstants.IdentityServerJwtBearerScheme)
                    .RequireAuthenticatedUser()
                    .Build());

            services.AddSignalR();

            services.AddResponseCompression(opts =>
            {
                opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                    new[] { "application/octet-stream" });
            });

            services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

            services.AddControllersWithViews();
            services.AddRazorPages();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseWebAssemblyDebugging();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseBlazorFrameworkFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseIdentityServer();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapHub<ChatHubTest>("/chatHubTest");

                endpoints.MapFallbackToFile("index.html");
            });
        }
    }

ChatHubTest.cs

    public class ChatHubTest : Hub
    {

        public async Task SendPrivateMessage(string receiver, string message)
        {
            string dateStamp = DateTime.Now.ToString();
            string sender = Context.User.Identity.Name;
            string senderConnectId = Context.ConnectionId;

            await Clients.User(receiver).SendAsync("ReceiveMessage", sender, receiver, "Send User: " + message, dateStamp);
            await Clients.User(sender).SendAsync("ReceiveEchoMessage", sender, receiver, "Send User: " + message, dateStamp);
            await Clients.All.SendAsync("ReceiveMessage", sender, receiver, "Send All: "+ message, dateStamp);
            await Clients.Client(senderConnectId).SendAsync("ReceiveMessage", sender, receiver, "Send ConnectId: " + message, dateStamp);

        }
    }

ChatTest.razor

@page "/chat/chatTest"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@implements IDisposable
@using Microsoft.AspNetCore.Authorization
@inject HttpClient http
@attribute [Authorize]

<h3 class="text-warning">Chat Test</h3>
<hr />

@foreach ((string Sender, string Receiver, string Message, string DateStamp) message in messages)
{
    <div> @message.Sender "to " @message.Receiver</div>
    <div>@message.Message</div>
    <div>@message.DateStamp</div>
    <p></p>
}

<div class="footer">
    <div class="row">
        <div class="col-8 form-group ml-3">
            <div>
                <input @bind="receiverEmail" class="w-100" placeholder="Recipient Email" />
            </div>
            <div>
                <textarea @bind="messageInput" placeholder="Message" rows="2" class="w-100 mt-1"></textarea>
            </div>
        </div>
        <div class="col-1">
            <button @onclick="Send" disabled="@(!IsConnected)" class="btn btn-success">
                Send
            </button>
        </div>
    </div>
</div>

@code{
    [CascadingParameter]
    private Task<AuthenticationState> authenticationStateTask { get; set; }

    [Parameter] public string ChatKey { get; set; }

    private HubConnection hubConnection;
    private List<(string, string, string, string)> messages = new List<(string Sender, string Receiver, string Message, string DateStamp)>();
    private string receiverEmail;
    private string messageInput;
    public bool IsConnected => hubConnection.State == HubConnectionState.Connected;
    private string errorMessage;
    private string authMessage;

    protected override async Task OnInitializedAsync()
    {

        hubConnection = new HubConnectionBuilder()
        .WithUrl(NavigationManager.ToAbsoluteUri("/chatHubTest"))
        .WithAutomaticReconnect()
        .Build();

        hubConnection.On<string, string, string, string>("ReceiveMessage", async (sender, receiver, message, datestamp) =>
        {
            messages.Add((sender, receiver, message, datestamp));
            StateHasChanged(); 
            await ScrollToBottom();

        });

        hubConnection.On<string, string, string, string>("ReceiveEchoMessage", async (sender, receiver, message, datestamp) =>
        {
            messages.Add((sender, receiver, message, datestamp));
            StateHasChanged();
            await ScrollToBottom();

        });

        hubConnection.On<string, string, string, string>("RecieveAllMessage", async (sender, receiver, message, datestamp) =>
        {
            messages.Add((sender, receiver, message, datestamp));
            StateHasChanged(); 
            await ScrollToBottom();

        });

        await hubConnection.StartAsync();

    }

    public async Task Send()
    {

        if (String.IsNullOrEmpty(receiverEmail) || String.IsNullOrEmpty(messageInput))
        {
            errorMessage = "Please use a valid recipient email and message";
        }
        else
        {
            errorMessage = "";
            await hubConnection.SendAsync("SendPrivateMessage", receiverEmail, messageInput);
        }
    }

    public void Dispose()
    {
        _ = hubConnection.DisposeAsync();
    }

    public async Task ScrollToBottom()
    {
        await JSRuntime.InvokeVoidAsync("scrollToStandardBottom");
    }
}

Program.cs

    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddHttpClient("BlazorHospital.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
                .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

            builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("BlazorHospital.ServerAPI"));

            builder.Services.AddApiAuthorization()
                .AddAccountClaimsPrincipalFactory<CustomUserFactory>();
            builder.Services.AddOptions();
            builder.Services.AddAuthorizationCore();

            await builder.Build().RunAsync();
        }
    }
BrennanConroy commented 4 years ago

By default the user identifier is the NameIdentifier claim. You can access the value used by SignalR in the Hub via Context.UserIdentifier. If you want to change that you can provide your own IUserIdProvider implementation to DI. https://docs.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1#use-claims-to-customize-identity-handling

fandango57 commented 4 years ago

Thanks Brennan,

The await Clients.User(Context.UserIdentifier).SendAsync does work to send a message back to sender. But the receiver is not receiving the message using Clients.User(receiverEmail) and my code does map the NameUserIdProvider:IUserIdProvider shown below (which works on server side MVC). I replaced that with the ClaimType.Email?Value as in your reference and neither works. You can see the NameUserIdProvider registered in startup class above and I replaced it with EmailBasedUserIdProvider.

Still no go. Maybe I'm having a bad hair day..... wait a minute I'm bald.

I'll investigate further.

    public class NameUserIdProvider : IUserIdProvider
    {
        public string GetUserId(HubConnectionContext connection)
        {
            return connection.User?.FindFirst(ClaimTypes.Name)?.Value;
        }
    }
//Replace above with
    public class EmailBasedUserIdProvider : IUserIdProvider
    {
        public virtual string GetUserId(HubConnectionContext connection)
        {
            return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
        }
    }
fandango57 commented 4 years ago

Well, I should have read the complete section you referred me to. I missed that you needed to add an Email claim to the user. After a quick check, it looks like its working, I'll review again tomorrow.

Thanks Brennan.