I am trying to get token authentication in a .net core app to work with signalR. The idea is, that a web frontend initiates a SignalR connection to the backend. The javascript signalR client adds a token to the SignalR connector as follows:
const options: IHttpConnectionOptions = {
accessTokenFactory : (): Promise<string> => {
return new Promise<string>((resolve) => {
this.log('providing token');
resolve(this.login.getToken());
});
}
};
this.connection = new HubConnectionBuilder()
.withUrl('/mqtthub', options)
.configureLogging(LogLevel.Debug)
.build();
So far this works fine, I actually see the token show up when the signalR connection is negotiated. Here is where I start to wonder though. I can get the SignalR endpoint to validate the token, but validated or not the connetion is established anyway. I would rather unauthenticated connections where rejected straight away. But that is of secondary importance..
When I override public OnConnectedAsync() in the SignalR hub I do not get an authenticated user. And calling a Method on the signalR connection (from within the web frontend) fails because I am using the attribute [Authorize("Bearer")] on my function in the hub. I have gone through tons of examples but the combination of SignalR and JWT Auth seems to be rather rare, even more so in a .Net Core 2.1 implementation.
My setup in Startup.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IUserData, UserData>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(jwtOpts => {
jwtOpts.TokenValidationParameters = TokenValidator.DefaultValidationParameters;
//jwtOpts.SecurityTokenValidators.Clear();
//jwtOpts.SecurityTokenValidators.Add(new TokenValidator());
});
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
services.AddMvc();
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
}
app.UseStatusCodePagesWithReExecute("/");
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseAuthentication();
// app.UseCors("CorsPolicy");
app.UseMvc();
app.UseSignalR(routes => { routes.MapHub<MQTTHub>("/mqtthub"); });
}
I have been using a custom JWT validator, now I using the system again. There does not seem to be anything wrong with validation as I see it is successfull. I am wondering though if certain content is needed in the JWToken to allow to authorize a user, I saw one sample put a whole bunch of claims into the token. Mine has the following content:
private string GenerateToken(string userid, string username)
{
_iat = ToEpoch(DateTime.Now);
_exp = ToEpoch(DateTime.Now.AddMinutes(20));
var payload = new JwtPayload
{
{"iat", _iat },
{"iss", _issuer},
{"exp", _exp},
{"nbf", _iat},
{"aud", "*"},
{"sub", userid},
{"name", username},
{"topicMask", $"^/{userid}/.*" },
{"scope", _issuer},
};
var jwtToken = new JwtSecurityToken(_jwtHeader, payload);
return JwtHandler.WriteToken(jwtToken);
}
Unfortunately I am rather new to ASP.NET and totally confused by all those middlewares. If anyone could point out what I missed out on ..
EDIT: I have a bit better understanding of the problem now. SignalR first negotiates. Here the token comes in the header and Token Auth works. Then when it actually connects a websocket in the second request, it sends the token again, but this time in a URL query parameter. And this is the request that is not authenticated.. So the question is how do I get the system to gather the token from a query parameter and authenticate.
This also leads to the question, why I would want to see my tokens in the query parameter. For one thing it leads to them showing up in the log which is not exactly a security feature. Maybe I am better off doing plain websockets after all..
UseAuthenticationwhich gets the token from query string and adds it to header. - Kahbazi