SignalR client doesn't connect to server with http protocol - asp.net-core

I've made .NET Core API with signalR, when I load app with https protocol, I can connect with signalR javascript client, but when I load app with http protocol - signalR js client can't connect to hub. CORS works fine.
My Code: Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.WithOrigins("http://localhost:3000")
.AllowAnyMethod()
.AllowCredentials()
.AllowAnyHeader());
});
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
services.AddControllers(options =>
{
options.EnableEndpointRouting = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
logger.LogInformation($"Started Configure with is Production mode:{env.IsProduction()}");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthorization();
app.UseAuthentication();
app.UseEndpoints(route =>
{
route.MapHub<ConnectionHub>("/chat");
route.MapControllers();
});
}
On my JS app:
var connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5066/chat") // WHEN I'M CHANGING TO HTTPS host - everything works fine
.build();
// Create a function that the hub can call to broadcast messages.
connection.on("broadcastMessage", function (name, message) { });
connection.start()
.then(function () {
connection.invoke("Send", name, messageInput.value);
})
.catch((error) => {
console.error(error.message);
});

Well , I resolved this problem adding options object to signalR client (add skipNegotiations: true) . To be honestly I hdont know what does it mean yet (tomorrow I'll read meaning and I'll write description of this property).
// Start the connection.
var connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5066/chat", {
skipNegotiation: false,
transport: signalR.HttpTransportType.WebSockets|
signalR.HttpTransportType.LongPolling |
signalR.HttpTransportType.serverSentEvents,
})
.build();
UPD: Well, It became works thanks to adding options object after url string.
About this object. At first signalR make negotiation (it's like some handshake between back and front) and if transport type is only websockets - you should skip it.

Related

Error with Google Login and ASP.NET Core Web API

I have a Blazor client with a Asp.net Core Web API using a custom implementation of JWT for authentication and I'm trying to implement an external identity provider (Google OAuth).
From Blazor when a user clicks the link to auth with google, is calling the following API endpoint:
public async Task<IActionResult> ExternalLogin([FromBody] string provider)
{
var redirectUrl = "/ExternalLoginCallback";
AuthenticationProperties properties = _repository.Account.ExternalLogin(provider, redirectUrl);
return Challenge(properties, provider);
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection(AppSettings.JWTSettings));
services.Configure<AppSettings>(Configuration.GetSection(AppSettings.EmailConfiguration));
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
services.ConfigureSqliteContext(Configuration);
services.ConfigureLoggerService();
services.ConfigureDependencyInjection(Configuration);
services.AddControllers();
services.AddAutoMapper();
services.AddIdentity<User, IdentityRole>(opt =>
{
opt.Password.RequiredLength = 0;
opt.Password.RequireDigit = false;
opt.Password.RequireUppercase = false;
opt.Password.RequiredUniqueChars = 0;
opt.Password.RequireNonAlphanumeric = false;
opt.User.RequireUniqueEmail = true;
opt.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<RepositoryContext>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddGoogle(GoogleDefaults.AuthenticationScheme, conf =>
{
var googleAuth = Configuration.GetSection("Google");
conf.ClientId = googleAuth["ClientId"];
conf.ClientSecret = googleAuth["ClientSecret"];
conf.SignInScheme = IdentityConstants.ExternalScheme;
});
if (Environment.IsDevelopment())
{
services.ConfigureAuthDevelopment(Configuration);
}
else
{
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("CorsPolicy");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
Edge Dev Console error:
Access to fetch at 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=******-o1gn4lob5hcknjggl10837m3ea5om5b4.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Flocalhost%3A44313%2Fsignin-google&scope=openid%20profile%20email&state=CfDJ8Ok9DyPEKDNCp3gn7utujrjiLfNm4KnoB6RC-cf3g01gBFzNSAqo54K-GaMudVevgRKaJZbcT5_O9S-jCGixs0i5SPY_CUxvM-l_DDsckOP0iHQMgyncA_-Ce_8vgCuxeozNkRWOHDJWar174-TkOgulJpYTr7b82MtOkUs3FOmiqpJ42YU2Q74y9imEFsgk8lynBRzb8Qqvh7P5kMuIg85QNnpjeO6lFbvefLDXPIzzE2r0n5sYlo1vzUG2sRbtRNgfThR6TvF-LPuMGygVCRI' (redirected from 'https://localhost:44313/api/account/ExternalLogin?provider=Google') from origin 'https://localhost:44313' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
dotnet.5.0.2.js:1 GET https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=*******-o1gn4lob5hcknjggl10837m3ea5om5b4.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Flocalhost%3A44313%2Fsignin-google&scope=openid%20profile%20email&state=CfDJ8Ok9DyPEKDNCp3gn7utujrjiLfNm4KnoB6RC-cf3g01gBFzNSAqo54K-GaMudVevgRKaJZbcT5_O9S-jCGixs0i5SPY_CUxvM-l_DDsckOP0iHQMgyncA_-Ce_8vgCuxeozNkRWOHDJWar174-TkOgulJpYTr7b82MtOkUs3FOmiqpJ42YU2Q74y9imEFsgk8lynBRzb8Qqvh7P5kMuIg85QNnpjeO6lFbvefLDXPIzzE2r0n5sYlo1vzUG2sRbtRNgfThR6TvF-LPuMGygVCRI net::ERR_FAILED
[]
Edge Dev Console network header
Request URL: https://localhost:44313/api/account/ExternalLogin?provider=Google
Request Method: GET
Status Code: 302
Remote Address: [::1]:44313
Referrer Policy: strict-origin-when-cross-origin
My assumption is that Challenge() produces a redirect which is then "blocked by CORS policy" from Blazor.
What would be the simplest yet more efficient way to implement it?

.NET CORE 3.1 ERROR 500 during WebSocket handshake

I am using SignalR on my .net core 3.1 API and it all works on localhost
hosting my app on ubuntu 20.4 will result in error 500 during handshake...
I am pretty sure I am missing something on my Apache configs...
error is just "Error during WebSocket handshake: Unexpected response code: 500" no extra data although -> options.EnableDetailedErrors = true
any idea why it doesn't work?
this my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
}).AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
});
services.AddControllers();
// Configure Compression level
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);
// Add Response compression services
services.AddResponseCompression(options =>
{
options.Providers.Add<GzipCompressionProvider>();
options.EnableForHttps = true;
});
services.AddTransient<IActionContextAccessor, ActionContextAccessor>();
JsonSerializerSettings jsonSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include,
// ContractResolver = new CamelCasePropertyNamesContractResolver()
};
jsonSettings.Converters.Add(new StringEnumConverter());
services.AddMvc().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
services.AddDistributedMemoryCache();
services.AddOptions();
services.Configure<IConfiguration>(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IDistributedCache cache)
{
app.UseResponseCompression();
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<AnalyticsHub>("/AnalyticsHub");
endpoints.MapHub<CampaignHub>("/CampaignHub");
endpoints.MapHub<LinkHub>("/LinkHub");
});
app.UseStaticFiles();
}
the error:

Failed to connect to SignalR in Blazor webassembly

I'm trying to connect to a SignalR service from my blazor webassembly client but this fails I think on CORS. This is the code in my razor file.
m_connection = new HubConnectionBuilder()
.WithUrl(myMircoServiceUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(userService.Token);
})
.WithAutomaticReconnect()
.Build();
await m_connection.StartAsync();
Then in the webassembly logging I see the following error:
Access to fetch at 'xxxx/negotiate?negotiateVersion=1' from origin 'http://localhost:5010' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I added the following CORS policy in my Blazor server configuration and something similar in the microservice config:
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
else
{
app.UseExceptionHandler(#"/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(policy => policy
.WithOrigins("http://localhost:5010")
.AllowAnyHeader()
.AllowAnyMethod());
app.UseClientSideBlazorFiles<Client.Program>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<Client.Program>(#"index.html");
});
Anybody got any idea what might be wrong?
Update 1
I now see the following error in the Chrome console:
dotnet.js:1 WebSocket connection to 'ws://localhost:5000/hubs/posts?id=9Jxs0DhP924zgw_eIeE9Lg' failed: HTTP Authentication failed; no valid credentials available
Update 2
I removed the [Authorize] attribute from the SignalR hub and now it connects. And I can send messages to the hub. Problem is there is a reason for this attribute, because I don't want that people can subscribe to messages that are not for them
Update 3
Still no progress. Looking at pulling out the authentication to a seperate microservice using IdentityServer4. Last status is I have the following startup routines:
Microservice: gist.github.com/njannink/15595b77ffe1c0593be1a555fa37f83f
Blazor server: gist.github.com/njannink/7302a888110e24d199ea45b66da4f26b
Blazor client: gist.github.com/njannink/add2568cbf48c8b3c070ccd4f28fd127
I've got the same errors with CORS and afterwards Websocket.
In my case the fallback longPolling was used as why the connection worked but the console logged the error HTTP Authentication failed; no valid credentials available.
If you use Identity Server JWT the following code solved the error for my case.
(The Code is from the Microsoft SignalR Documentation - Authentication and authorization in ASP.NET Core SignalR - Identity Server JWT authentication)
services.AddAuthentication()
.AddIdentityServerJwt();
// insert:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
ConfigureJwtBearerOptions>());
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string name, JwtBearerOptions options)
{
var originalOnMessageReceived = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await originalOnMessageReceived(context);
if (string.IsNullOrEmpty(context.Token))
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
}
};
}
}
Important: Your Route has to start with hubs for the Options to trigger!
(see Line path.StartsWithSegments("/hubs")))
app.UseEndpoints(e =>
{
...
e.MapHub<ChatHub>("hubs/chat");
});
In my case, ASP.NET Core 2.2 I have an API from which I want to be able to use SignalR from the API to connect to my client application.
I have Projects for
Web API
IdentityServer4
MVC Client
With ASP.NET Core Identity as the for user management
In order for your user to be authenticated you need to implement a IUserIdProvider like this
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
//TODO: Implement USERID Mapper Here
//throw new NotImplementedException();
//return whatever you want to map/identify the user by here. Either ID/Email
return connection.User.FindFirst("sub").Value;
}
}
With this I make sure I am pushing along the ID/Email to a method I am calling either from the Server or Client. Although I can always use the .User on the HubContext and it works fine.
In my Web API Startup.cs file I came up with
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy =>
{
policy.WithOrigins(Configuration.GetSection("AuthServer:DomainBaseUrl").Get<string[]>())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed((_) => true)
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, UserManager<AppUser> userManager,
RoleManager<IdentityRole> roleManager){
app.UseCors();
}
NOTE
Configuration.GetSection("AuthServer:DomainBaseUrl").Get() retrieves the list of domains to allow CORS for from a config file.
And I did this configuration in My Client App COnfigureService Method
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy => {
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.SetIsOriginAllowed((host) => true);
policy.AllowAnyOrigin();
});
});
I hope this helps your situation.
The best solution is indeed as Ismail Umer described using a seperate authentication service using something like IdentityServer4. And use this service in all other services. This is something I will do in a next iteration.
As short term solution I temporary moved the blazor server part into my api service and use a dual authentication method (JWT header or cookie).
var key = Encoding.UTF8.GetBytes(m_configuration[#"SecurityKey"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = #"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
})
.AddCookie();
// TODO: For time being support dual authorization. At later stage split in various micro-services and use IdentityServer4 for Auth
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
This is problem with Microsoft.AspNetCore.SignalR.Client 3.1.3.
You can read about it here in comments.
You can wait for update or temporarly fix this issue:
Disable negotiation
Set WebSocket transport explicitly
Modify query url
Add OnMessageReceived handler
Client side:
var token = await GetAccessToken();
var hubConnection = new HubConnectionBuilder()
.WithUrl($"/notification?access_token={token}", options =>
{
options.SkipNegotiation = true;
options.Transports = HttpTransportType.WebSockets;
options.AccessTokenProvider = GetAccessToken;
})
.Build();
Server side:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// ...
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/notification", System.StringComparison.InvariantCulture)))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},
};
});
}

.netcore web API Post does not work without SSL

I have an MVC core 2.2 application with Controllers, API controllers, and some Views. Some of the Views make use of the API endpoints within the same application. All was ok until I enabled TLS 1.2 in my windows server.
Now all endpoints decorated as GET methods work. But all POSTs have stopped working with chrome reporting this POST 'link' net::ERR_CONNECTION_RESET.
Other browsers catch exception at fail and display my error text with object
$.ajax({
url: "/api/Cart",
method: "post",
data: JSON.stringify(vm),
contentType: "application/json"
}).done(function(result) {
console.log(result);
}).fail(function(ex) {
console.log("Error occured while adding to cart" + ex)
});
I want to be able to POST even without SSL. I have disabled TLS 1.2 and restarted but still the same result.
If I browse the site with https://, POST and GET endpoints all work perfectly but if I browse with HTTP://, only GET endpoints work, all POST endpoints do not work.
I have combed the internet for almost 5 hours for something I thought would be simple.
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration.GetConnectionString("DataConnection");
services.AddDbContext<DataContext>(options => options.UseSqlServer(connectionString));
//Inject Connection String to other Classes
services.AddSingleton(_ => connectionString);
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<DataContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
});
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddMediatR(typeof(CreateProductCommand).Assembly, typeof(CreateProductCommandHandler).Assembly);
services.AddAutoMapper(typeof(MappingProfile));
// Add memory cache services
services.AddMemoryCache();
services.AddMvc(o =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
o.Filters.Add(new AuthorizeFilter(policy));
}) .SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_2);
}
// HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
var serviceProvider = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope().ServiceProvider;
DataContextSeed.Initialize(serviceProvider);
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Above is the Startup class and a sample post is here
[HttpPost]
public IActionResult Post([FromBody] SaleItemDTO md)
{
if(md != null)
{
if(md.Quantity <= md.Stock)
{
_sales.SalesPerson = User.Identity.Name;
_sales.SalesType = md.SalesType;
return Ok(_sales.ItemsInDb);
}
}
return BadRequest(new { Message = "Not Valid Content posted" });
}
Help with your thoughts.

Run a custom method immediately after Azure AD Authentication

I have an ASP.NET Core Web App which successfully uses Azure AD Authentication. I would like to run a process immediately after a user logs in. I thought I might somehow handle the Redirect URI specified in the Azure app registration but I couldn't figure it out as much of the login process is nicely handled by the .AddAzureAd() method in my Startup.cs.
Can anyone suggest an easy way to call a method or redirect to a razor page after authentication? Preferably something which would not be circumvented by specifying a returnUrl in the initial request.
Update
Between posting the question and seeing the answers I found what might be considered a hack:
Basically I created a service and injected it into my _LoginPartial.cshtml page and then call a method on the service.
...
#inject MyService myService
...
#if (User.Identity.IsAuthenticated)
{
await MyService.MyCustomMethod();
...
}
For running code or changing the redirect url, you could configure OpenIdConnectOptions.
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = ctx =>
{
ctx.Properties.RedirectUri = "/Home/Privacy";
return Task.CompletedTask;
},
};
});
If you want to run code after authentication, you could place your code in the OnTokenValidated.
If you want to change the uri, you could replace /Home/Privacy.
You can define the route in Startup.cs file. I used the sample here.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
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)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Contact}/{id?}");
});
}