I cant seem to get SignalR core to work with cookie authentication. I have set up a test project that can successfully authenticate and make subsequent calls to a controller that requires authorization. So the regular authentication seems to be working.
But afterwards, when I try and connect to a hub and then trigger methods on the hub marked with Authorize
the call will fail with this message: Authorization failed for user: (null)
I inserted a dummy middleware to inspect the requests as they come in. When calling connection.StartAsync()
from my client (xamarin mobile app), I receive an OPTIONS
request with context.User.Identity.IsAuthenticated
being equal to true. Directly after that OnConnectedAsync
on my hub gets called. At this point _contextAccessor.HttpContext.User.Identity.IsAuthenticated
is false. What is responsible to de-authenticating my request. From the time it leaves my middleware, to the time OnConnectedAsync is called, something removes the authentication.
Any Ideas?
Sample Code:
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await this._next(context);
//At this point context.User.Identity.IsAuthenticated == true
}
}
public class TestHub: Hub
{
private readonly IHttpContextAccessor _contextAccessor;
public TestHub(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public override async Task OnConnectedAsync()
{
//At this point _contextAccessor.HttpContext.User.Identity.IsAuthenticated is false
await Task.FromResult(1);
}
public Task Send(string message)
{
return Clients.All.InvokeAsync("Send", message);
}
[Authorize]
public Task SendAuth(string message)
{
return Clients.All.InvokeAsync("SendAuth", message + " Authed");
}
}
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyContext>(options => options.UseInMemoryDatabase(databaseName: "MyDataBase1"));
services.AddIdentity<Auth, MyRole>().AddEntityFrameworkStores<MyContext>().AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options => {
options.Password.RequireDigit = false;
options.Password.RequiredLength = 3;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.User.RequireUniqueEmail = true;
});
services.AddSignalR();
services.AddTransient<TestHub>();
services.AddTransient<MyMiddleware>();
services.AddAuthentication();
services.AddAuthorization();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<MyMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseSignalR(routes =>
{
routes.MapHub<TestHub>("TestHub");
});
app.UseMvc(routes =>
{
routes.MapRoute(name: "default", template: "{controller=App}/{action=Index}/{id?}");
});
}
}
And this is the client code:
public async Task Test()
{
var cookieJar = new CookieContainer();
var handler = new HttpClientHandler
{
CookieContainer = cookieJar,
UseCookies = true,
UseDefaultCredentials = false
};
var client = new HttpClient(handler);
var json = JsonConvert.SerializeObject((new Auth { Name = "craig", Password = "12345" }));
var content = new StringContent(json, Encoding.UTF8, "application/json");
var result1 = await client.PostAsync("http://localhost:5000/api/My", content); //cookie created
var result2 = await client.PostAsync("http://localhost:5000/api/My/authtest", content); //cookie tested and works
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/TestHub")
.WithConsoleLogger()
.WithMessageHandler(handler)
.Build();
connection.On<string>("Send", data =>
{
Console.WriteLine($"Received: {data}");
});
connection.On<string>("SendAuth", data =>
{
Console.WriteLine($"Received: {data}");
});
await connection.StartAsync();
await connection.InvokeAsync("Send", "Hello"); //Succeeds, no auth required
await connection.InvokeAsync("SendAuth", "Hello NEEDSAUTH"); //Fails, auth required
}
It looks like this is an issue in the WebSocketsTransport where we don't copy Cookies into the websocket options. We currently copy headers only. I'll file an issue to get it looked at.