SignalR + Angular: how to add Bearer token to Http Headers - asp.net

I made an asp.net core 2.0 SignalR Hub which uses Bearer Token for Authentication. Now I'm a bit lost on how to connect to it via the SignalR Angular 5 client. I actually can connect if I remove authorization from the Hub, so the connection is working, now I believe I just need to add the Authorization Bearer to the Http Headers of the connection.
The SignalR client reference in the package.json file of my Angular 5 project: "#aspnet/signalr-client": "^1.0.0-alpha2-final"
My Angular component:
import { Component, OnInit } from '#angular/core';
import { finalize } from 'rxjs/operators';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { ToastrService } from 'ngx-toastr';
import { AuthenticationService } from '../core/authentication/authentication.service';
import { HubConnection } from '#aspnet/signalr-client';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
quote: string;
isLoading: boolean;
jwtToken:string;
private hubConnection: HubConnection;
constructor(
private _http: HttpClient,
private _auth : AuthenticationService,
private _toastr: ToastrService) { }
ngOnInit() {
this.isLoading = false;
this.jwtToken = this._auth.currentToken;
this.hubConnection = new HubConnection('http://localhost:27081/hub/notification/');
this.hubConnection
.start()
.then(() => console.log('Connection started!'))
.catch(err => console.error('Error while establishing connection :(', err));
this.hubConnection.on("send", data => {
console.log(data);
});
}
showToastr(){
this._toastr.success('Hello world!', 'Toastr fun!');
}
}
Due to reading similar questions I tried: this.hubConnection.Headers.Add("token", tokenValue); but it doesn't work, the Headers property doesn't exist.
How can I add the Bearer token to the Http Headers of the HubConnection?
Thanks for any help

To do this with #aspnet/signalr (^1.1.4) you can use the following code
const options: IHttpConnectionOptions = {
accessTokenFactory: () => {
return "Token is resolved here";
}
};
const connection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl(`${environment.apiUrl}/notify`, options)
.build();
Also add an annotation to your Hub
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
As a side note, SignalR when using the websocket protocol does not seem to attach the Bearer token as a header and instead adds it to the request URL as an 'access_token' parameter, this requires you to configure your authentication to handle this token when signalR chooses to use ws.
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
x.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("/notify")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});

From reading their source code and tests, it looks like you can provide an options object containing your access token, like so
var options = {
transport: transportType,
logging: signalR.LogLevel.Trace,
accessToken: function () {
return jwtToken;
}
};
hubConnection = new signalR.HubConnection('/authorizedhub', options);
hubConnection.start();
The code in particular in the test file here

I couldn't find a way to solve using angular but I did it using asp.net following this article.
This is what I did: Now to connect I pass the jwt token in the querystring and specify transport type:
const options = {
transport: TransportType.WebSockets
};
this.hubConnection = new HubConnection('http://localhost:27081/hub/notification/?token='+this.jwtToken, options);
And then in the startup.cs > ConfigureServices() :
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
cfg.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Query.TryGetValue("token", out StringValues token)
)
{
context.Token = token;
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var te = context.Exception;
return Task.CompletedTask;
}
};
});

Related

Keep getting 401 on authorization with SignalR

I have been trying to create a sample Test app with SignalR but I have been extremely unsuccessful with authentication.
I am on .NET 6, and my Program.cs code looks like this.
Program.cs
using HubTestApp.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSignalR();
builder.Services
.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{TenantId}/";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
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("/Test")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<SimpleHub>("/Test");
app.Run();
My hub code is pretty simple:
namespace HubTestApp.Hubs
{
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
internal class SimpleHub : Hub<ISimpleClient>
{
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task EchoMessage(string message) => await Clients.All.ReceiveMessage(message);
}
}
And this is my client code:
namespace HubTestClient
{
using Microsoft.AspNetCore.SignalR.Client;
public class MockClient
{
private const string Token = "Bearer <JWT from AAD>";
private readonly HubConnection hubConnection;
public MockClient()
{
// Notice here I have tried to pass the token in various ways, all to no avail.
this.hubConnection = new HubConnectionBuilder()
.WithUrl($"http://localhost:5110/Test?access_token={MockClient.Token}", options =>
{
options.AccessTokenProvider = () => Task.FromResult(MockClient.Token);
options.Headers.Add("Authorization", MockClient.Token);
})
.Build();
this.hubConnection.On<string>("ReceiveMessage", (message) =>
{
Console.WriteLine($"Received message: '{message}'");
});
}
public async Task StartClient()
{
await hubConnection.StartAsync();
Random rng = new Random();
while (true)
{
string messageToEcho = $"Sending random number '{rng.Next()}'";
Console.WriteLine(messageToEcho);
try
{
await hubConnection.InvokeAsync("EchoMessage", messageToEcho);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
// Delay 5 seconds between hitting the hub.
await Task.Delay(5000);
}
}
}
}
I continuously get the message "Failed to invoke 'X' because user is unauthorized." I have made sure the token I got is valid. So, I'm bashing my head over this, completely confused where I am going wrong. Thank you in advance!

Using PKCE auth on swagger with an API that accepts bearer auth

I'm using Auth0 PKCE authentication on my NuxtJs Application which has a backing Dotnet 6 API. I've set up the API to accept a Bearer token as shown below. For ease of use, I wanted to set up my swagger environment to use PKCE auth also which I've done but the token never authenticates successfully to the Dotnet API. The access token I get from the UI and from postman successfully accessed the API because I am getting the access token and sending it as an authorization header. I attempted to also configure my web API to use OAuth2 along with JWT but the OAuth2 code is never getting hit.
I encountered the same issue originally in postman until I added my API Audience for the token to correctly generate but swagger doesn't seem to allow me to provide an audience.
How do I adjust swagger to use the PKCE auth but generate a token that works for the API?
Swagger setup:
private static void ConfigureSwaggerGen(SwaggerGenOptions options, IConfiguration config)
{
options.OperationFilter<SwaggerDefaultValues>();
options.UseDateOnlyTimeOnlyStringConverters();
var xmlFile = $"{typeof(Program).GetTypeInfo().Assembly.GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
options.DocInclusionPredicate((docName, apiDesc) => apiDesc.RelativePath != null &&
!string.IsNullOrEmpty(apiDesc.GroupName) &&
apiDesc.RelativePath.Contains($"api/{docName}/"));
options.ExampleFilters();
options.TagActionsBy(api => new[]
{api.GroupName});
var oauthAuthority = config[$"{Constants.ConfigSettings}:{Constants.OAuthAuthority}"] ??
config[Constants.OAuthAuthority];
var securitySchema = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
In = ParameterLocation.Header,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl =
new Uri(
$"https://{oauthAuthority}/authorize"),
TokenUrl = new Uri($"https://{oauthAuthority}/oauth/token")
}
},
Description = Constants.SwaggerAuthDescription,
Name = Constants.SwaggerAuthName
};
options.OperationFilter<SecurityRequirementsOperationFilter>();
options.AddSecurityDefinition(Constants.SwaggerAuthName, securitySchema);
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{Id = Constants.SwaggerAuthName, Type = ReferenceType.SecurityScheme}
},
new List<string>()
}
});
}
private static void ConfigureSwaggerUi(SwaggerUIOptions options, IApiVersionDescriptionProvider provider,
IWebHostEnvironment env, IConfiguration config)
{
options.OAuthAppName("TestApp");
options.OAuthClientId(config[$"{Constants.ConfigSettings}:{Constants.OAuthClientId}"] ??
config[Constants.OAuthClientId]);
options.OAuthClientSecret(config[$"{Constants.ConfigSettings}:{Constants.OAuthClientSecret}"] ??
config[Constants.OAuthClientSecret]);
options.OAuthUsePkce();
options.OAuthScopes("profile", "openid", "api");
options.EnablePersistAuthorization();
options.OAuthConfigObject = new OAuthConfigObject
{
ClientId = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientId}"] ??
config[Constants.OAuthClientId],
ClientSecret = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientSecret}"] ??
config[Constants.OAuthClientSecret],
AppName = "TestApp",
Scopes = new List<string> {"profile", "openid", "api"},
AdditionalQueryStringParams = new Dictionary<string, string>
{
{
"audience", config[$"{Constants.ConfigSettings}:{Constants.OAuthAudience}"] ??
config[Constants.OAuthAudience]
}
},
UsePkceWithAuthorizationCodeGrant = true
};
if (env.IsProduction()) options.SupportedSubmitMethods();
foreach (var description in provider.ApiVersionDescriptions)
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
Dotnet Auth Configuration:
private static void ConfigureAuth(IServiceCollection services, IConfiguration config)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var authority = config[$"{Constants.ConfigSettings}:{Constants.OAuthAuthority}"] ??
config[Constants.OAuthAuthority];
options.Authority = $"https://{authority}";
options.Audience = config[$"{Constants.ConfigSettings}:{Constants.OAuthAudience}"] ??
config[Constants.OAuthAudience];
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = config[$"{Constants.ConfigSettings}:{Constants.OAuthAudience}"] ??
config[Constants.OAuthAudience],
ValidIssuer = $"https://{authority}"
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
if (context.SecurityToken is not JwtSecurityToken token) return Task.CompletedTask;
if (context.Principal?.Identity is ClaimsIdentity identity)
identity.AddClaim(new Claim("access_token", token.RawData));
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
}
};
})
.AddOAuth("Auth0", options =>
{
var authority = config[$"{Constants.ConfigSettings}:{Constants.OAuthAuthority}"] ??
config[Constants.OAuthAuthority];
options.ClientId = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientId}"] ??
config[Constants.OAuthClientId];
options.ClientSecret = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientSecret}"] ??
config[Constants.OAuthClientSecret];
options.CallbackPath = new PathString("/auth/loggedin");
options.AuthorizationEndpoint = $"https://{authority}/authorize";
options.TokenEndpoint = $"https://{authority}/oauth/token";
options.UserInformationEndpoint = $"https://{authority}/userinfo";
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadAsStringAsync();
var jUser = JsonSerializer.SerializeToElement(user);
context.RunClaimActions(jUser);
},
OnRedirectToAuthorizationEndpoint = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
return Task.CompletedTask;
},
OnAccessDenied = context =>
{
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
return Task.CompletedTask;
}
};
});
}

.NET 6.0 JWT token is not recognized from localhost, but it IS recognized through SwaggerUI-- what's up?

Problem
I have a Target Framework: .NET 6.0 API backend with an exposed API with Identity Framework implemented in it.
I can successfully obtain data 0 issue on unauthorized API Endpoints with this above them on both SwaggerUI AND my localhost frontend. [AllowAnonymous]
When it comes to authorized API Endpoints however it is a totally different story.
SwaggerUI has NO PROBLEM AT ALL when I put my generated "JWT" into the "Authorize" padlock on the side of the screen.
Localhost JWT are simply not accepted or seen or understood or something I totally don't understand.
Request seems fine
Screenshot of the API request I am sending over has the authorization with the token, I also took the token to JWT io and it can parse out the name without problem.
I've also tried removing or adding "bearer" to the authorization header sent over and that makes no difference.
Screencap of request:
https://i.imgur.com/CY7F32o.png
My entire Startup.cs
(I know showing anything less would make it harder for you all to help me so >_< )
namespace API
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(opt =>
{
opt.UseLazyLoadingProxies();
opt.UseMySql(Configuration.GetConnectionString("DefaultConnection"), ServerVersion.AutoDetect(Configuration.GetConnectionString("DefaultConnection")), opt => opt.EnableRetryOnFailure());
});
services.AddIdentity<UserModel, IdentityRole>(options =>
{
//options.Password.RequiredLength = 5;
//options.Password.RequireLowercase
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<DbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = new PathString("/api/contests");
options.AccessDeniedPath = new PathString("/api/contests");
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
services.Configure<SmtpSettings>(Configuration.GetSection("SMTP"));
services.AddSingleton<IEmailManager, EmailManager>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddControllers(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "API", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please insert JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
//Allow localhost to actually contact the server.
services.AddCors(opt =>
{
opt.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("WWW-Authenticate")
.WithOrigins("http://localhost:3000", "http://localhost:5000", "https://localhost:5000")
.AllowCredentials();
});
});
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["TokenKey"]));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
//"Normal" API Auth.
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = true, //~5 minute leeway?
ClockSkew = TimeSpan.Zero //force time.
};
//SignalR auth.
opt.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}).AddDiscord(options =>
{
options.CorrelationCookie.SameSite = SameSiteMode.Lax;
options.ClientId = Configuration["Discord:ClientId"];
options.ClientSecret = Configuration["Discord:ClientSecret"];
options.Scope.Add("email");
//options.CallbackPath = "/";
});
services.AddScoped<IJwtGenerator, JWTGenerator>();
services.AddScoped<IUserAccessor, UserAccessor>();
services.AddScoped<IImageAccessor, ImageAccessor>();
services.Configure<Infrastructure.Images.CloudinarySettings>(Configuration.GetSection("Cloudinary"));
services.AddRazorPages().AddRazorRuntimeCompilation();
services.AddServiceLayer();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseMiddleware<ErrorHandlingMiddleware>();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
});
app.UseCors(c => c.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToController("Index", "Fallback");
});
}
}
}

ASP.NET SignalR - 401 Unauthorized - JWT Authorization

I'm trying to implement SignalR support in an ASP.NET / Angular App, but I'm having difficulties with the JWT-Authentication.
First I'm appending the token in the appropriate request, which can be seen in the server code down below.
This seems to work fine, but I always get HTTP error 401 (Unauthorized) when trying to establish the websocket connection. When hitting a breakpoint in the code above, I can see that the token is assigned correctly:
All the other HTTP endpoints have the [Authorize] annotation, where the User object is accessible as excepted.
Here's a screenshot of the HTTP traffic. It's the same bearer token that works for all the other endpoints, but here it says "invalid token":
The server uses the Microsoft.AspNetCore.SignalR package, Version 1.1.0
Angular Client uses "#microsoft/signalr": "^5.0.7"
Here's the client code where the token is added when establishing the connection:
public startConnection = () => {
const options: IHttpConnectionOptions = {
accessTokenFactory: () => {
return this._settings.authorization?.access_token;
}
};
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl('http://localhost:8990/apphub', options)
.build();
this.hubConnection
.start()
.then(() => console.log('Connection started'))
.catch(err => console.log('Error while starting connection: ' + err))
}
This is the startup code to add JWT authentication:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
Log.Logger.Information("Authentication - Token validated.");
return Task.CompletedTask;
},
OnMessageReceived = ctx =>
{
Log.Logger.Information("Authentication - Message received.");
var tokenInQuery = ctx.Request.Query.TryGetValue("authorization", out var queryToken);
var tokenInHeader = ctx.Request.Headers.TryGetValue("authorization", out var headerToken);
if (tokenInQuery || tokenInHeader)
{
var path = ctx.HttpContext.Request.Path;
if (path.StartsWithSegments("/apphub", StringComparison.OrdinalIgnoreCase))
{
if(!string.IsNullOrEmpty(queryToken))
{
ctx.Token = queryToken;
}
else if(!string.IsNullOrEmpty(headerToken))
{
ctx.Token = headerToken;
}
}
}
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
Log.Logger.Information("Authentication - Authentication failed.");
return Task.CompletedTask;
}
};
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["SecurityKeyIssuer"],
ValidAudience = Configuration["SecurityKeyAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecurityKey"]))
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddMemoryCache();
// services.AddCors();
services.AddCors(o => o.AddPolicy("AllowAnyOrigin", builder =>
{
builder.WithOrigins("http://locahost:4000")
//.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed((x) => true)
.AllowCredentials();
}));
Any ideas?

JWT + SignalR on ASP Core 3 resulting in 401 Unauthorized

If I use http calls outside of signalr, such as with postman or httpclient, I am able to have my token validated successfully on the server. It's when I try to connect through my signalr hub that the token is not passing authorization.
Bearer was not authenticated. Failure message: No SecurityTokenValidator available for token: Bearer MyTokenFooBar
My service setup is:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddControllers();
services.AddHealthChecks();
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder => { builder.ConnectionString = _configuration.GetConnectionString("DefaultConnection"); }));
services.AddIdentity<ApplicationUser, IdentityRole>(setup =>
{
// foo
}).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = _configuration["Jwt:Issuer"],
ValidAudience = _configuration["Jwt:Audience"],
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])),
ValidateLifetime = false
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var path = context.HttpContext.Request.Path;
if (!path.StartsWithSegments("/chat")) return Task.CompletedTask;
var accessToken = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrWhiteSpace(accessToken) && context.Scheme.Name == JwtBearerDefaults.AuthenticationScheme)
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
services.AddSignalR(options => { options.EnableDetailedErrors = true; });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(options =>
{
options.MapHealthChecks("/health");
options.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR(options => { options.MapHub<ChatHub>("/chat"); });
}
I use a basic http auth header for the initial connection, which will sign the user into identity and generate a jwt token as a response for use in future calls.
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login()
{
var (headerUserName, headerPassword) = GetAuthLoginInformation(HttpContext);
var signInResult = await _signInManager.PasswordSignInAsync(headerUserName, headerPassword, false, false);
if (!signInResult.Succeeded)
{
return Unauthorized();
}
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SuperTopSecretKeyThatYouDoNotGiveOutEver!"));
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(signingCredentials: signingCredentials);
var handler = new JwtSecurityTokenHandler();
var token = handler.WriteToken(jwt);
return new OkObjectResult(token);
}
And my client (a console application) is setup to cache this token and use it in future signalr calls as such:
Get the token:
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encoding.GetBytes($"{userName}:{password}")));
var response = await _client.SendAsync(request); // this goes to the login action posted above
_token = await response.Content.ReadAsStringAsync();
...
_hubConnection = new HubConnectionBuilder()
.WithUrl(new Uri(_baseAddress, "chat"),
options => { options.AccessTokenProvider = () => Task.FromResult(_token); }) // send the cached token back with every request
.Build();
// here is where the error occurs. 401 unauthorized comes back from this call.
await _hubConnection.StartAsync();
Resolved.
The issue was that I was overriding the OnMessageReceived handler of the JwtBearerHandler and then having it read the incoming token myself... but the token I was passing it included the prefix Bearer, which when parsed by the above handler did not match the known token for the existing user.
Simply removing my override of OnMessageReceived and letting AspNetCore's deafult implementation of the JwtBearerHandler do its job allowed the token parsing to work correctly.

Resources