SignalR "Error during WebSocket handshake" sometimes - asp.net-core

My application is having issues accepting the handshake about half the time in my production environment. On my local machine and in my staging environment, which is the same as production, it works every time.
It is a .Net Core 2.1 app using aspnet-signalr/1.1.4
I am not configuring SignalR to use any specifics in my startup.s
app.UseSignalR(routes => { routes.MapHub<PortfolioHub>("/loadpagedetailsjob"); });
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Dashboard}/{action=Index}/{id?}");
});
my PortfolioHub is just a direct implementation of the Hub class
and in my page
var connection = new signalR.HubConnectionBuilder()
.withUrl('/loadpagedetailsjob')
.configureLogging(signalR.LogLevel.Information)
.build();
connection.start().then(function () {
connection.invoke("Subscribe");
});
It looks like it tries to negotiate web socket then long polling and both fail. But the requests return 200
Since it is only happening sometimes I am having issues resolving the issue. My only suspicion at this point is since my environment is in AWS behind a load balance that the negotiate requests are routed to different servers which might cause the issue?
Any help is appreciated.

SignalR requires that one connection is always handled by the same server. The typical solution is to configure the load balancer to use sticky sessions. With sticky sessions enabled, the load balancer will route requests of the same user to the same backend server.
https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-3.1

Related

ASP.Net Identity Login Redirect Enforce Protocol (Https) Part 2 (.Net 6++)

Prior reference (.Net Framework/ASP.Net MVC): ASP.Net Identity Login Redirect Enforce Protocol (Https)
It seems this is still an "issue" in .Net 6+. There are cases where the return url constructed by the infrastructure results in an http scheme/protocol instead of https for oauth/external logins (Google, etc). This obviously fails because it must be https.
While I haven't gone deep into things, because I haven't found the source code for it (yet?), it's likely the same "issue" - at the app level, it doesn't "see" a https request (because SSL is offloaded somewhere) and therefore the url created "matches" the scheme/protocol, resulting in an http redirect url.
End of day, whatever hosting infrastrucutre/configuration my host has is in place is beyond my control. Therefore, the ultimate goal is to force https (hard code, skip/override whatever scheme/protocol check/eval in place).
There's nothing special in my setup and it's working fine in local/dev (https) testing. It's only when the application is finally hosted (production):
In startup program.cs this is the only related code I have for external login (along with the scaffolding/templates of the identity package):
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<my_db_context>();
builder.Services.AddAuthentication().AddGoogle(goog =>
{
goog.ClientId = builder.Configuration["GoogleAuthClientId"];
goog.ClientSecret = builder.Configuration["GoogleAuthClientSecret"];
});
The issue:
the origin is https
the redirect uri sent to Google Auth is http - this will always fail
Can anyone point me to relevant docs/source on how to add/override options in .Net 6 and above? (similar to prior implementations in .Net Framework/MVC)?
The answer is in the comment by #Tratcher:
Official Ref: Configure ASP.NET Core to work with proxy servers and load balancers
Essentially: ForwardedHeadersMiddleware
For my specific case:
In some cases, it might not be possible to add forwarded headers to
the requests proxied to the app. If the proxy is enforcing that all
public external requests are HTTPS, the scheme can be manually set
before using any type of middleware:
...
app.Use((context, next) => {
context.Request.Scheme = "https";
return next(context); });
...

Microsoft.Identity.Web.UI works locally but not in App Service

I've been trying to add auth to my web app following: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC/1-1-MyOrg
If I run locally with appsettings.Development.json via dotnet run, I can log in using my organization credentials as expected. When I containerize and deploy to my Web App in Azure, I do not get logged in successfully. The url in the browser stays at /signin-oidc and goes to the Error page from the default Razor pages app.
The App Service logs have messages saying .AspNetCore.Correlation.OpenIdConnect.[key?] cookie not found
[Update] The auth flow works on my phone but not on desktop.
Why would the same code work locally but not deployed?
Why isn't the cookie found?
Why does it work on iOS but not Windows?
Try to remove app.UseCookiePolicy(); in startup class.
Take a look here
Or the problem was IdentityServer was still using AddDeveloperSigningCredential
You can add a certificate in your code it will work perfectly
Refer here for more info
tl;dr - Setting the same-site cookie options in the authentication settings fixed this.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.NonceCookie.SameSite = SameSiteMode.Unspecified;
options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
});
This seems to be because the App Service enforces TLS-only and handles the TLS termination. Because of this, requests to the application are always HTTP even though the browser URL is HTTPS. This causes a number of problems, the first was that the redirect URL never matched since the Identity library uses the request scheme to form the redirect URL. I had "fixed" this by injecting a redirect URL rewrite:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnRedirectToIdentityProvider += _fixRedirect;
});
...
private async Task _fixRedirect(RedirectContext context)
{
context.Request.Scheme = "https";
if(!context.ProtocolMessage.RedirectUri.StartsWith("https"))
context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http", "https");
await Task.CompletedTask;
}
However, the correlation cookie also seems to use the same HTTP request so that when the redirect comes back, the HTTPS doesn't match. Relaxing the SameSiteMode explicitly tells the browser(?) to allow the differing schemes. This is why my phone, with a different browser with different cookie policy, worked while desktop did not. Running locally using the dev-certs allowed the schemes to match since the app was terminating the TLS rather than the App Service.
I hooked into the CookiePolicyOptions.OnAppendCookie event to inspect the cookies while debugging to find this out.

Angular 11, HMR, wrong schema set, when used behind a asp.net core app with UseSpa

I have an asp.net core app that uses:
app.UseSpa(x =>
{
if (env.IsDevelopment())
{
x.Options.SourcePath = "ClientApp";
x.UseProxyToSpaDevelopmentServer("http://localhost:4200");
}
else
{
// x.Options.SourcePath
x.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
OnPrepareResponse = context =>
{
context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
context.Context.Response.Headers.Add("Expires", "-1");
}
};
}
});
angular is started with:
ng serve which then serves the site on http://localhost:4200
The --public-host does not seem to respect the schema.
The docs says: The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies.
My API is hosted in the asp.net core app.
The browser tries to connect to: https://localhost:4200/sockjs-node/info?t=1605229799939 ... which is not running. Notice the https, which is gets from the asp.net core app which is being served over https.
I can get it to work if I start the angular dev server with --ssl, trust the certicate in my certificate store in Windows, so .NET trusts that certificate. The issues is that there is no override for the UseSpa as I have found, to trust self signed certificates, when running in development.
I want the setup to be as clean as possible, with the least amount of hacks, like trusting self signed certs on local machine.
Is there any way to let the HMR/nodejs/angular-cli know what it needs to connect to?
Well ... setting --public-host to localhost:5000 solved the issue.
Then it's just going though the asp.net core app which acts as a proxy. Not sure why I didn't think of that before creating this issue.

UrlHelper returning http links on Azure App Service

I have a service that when deployed on Azure App Services returns http links instead of https links when using UrlHelper. When testing on my development machine it returns https links as expected, and the service is available and accessed through https requests.
An example of the type of route from my startup I'm trying to use is:
routes.MapRoute(
"FooBar",
"api/Foo/{Id}/Bar");
The link is then constructed using:
IUrlHelper _urlHelper = // Injected into class via service registration
int id = 42; // Arbitrary value for example
_urlHelper.Link("FooBar", new {Id = id});
When running on my local machine using Docker on Windows from Visual Studio I get a link of https://localhost:1234/api/Foo/42/Bar, but on my deployed Linux Container App Service on Azure I get http://my-app-name.azurewebsites.net/api/Foo/42/Bar.
I don't know what I'm doing wrong to get an http link instead of an https link, and would appreciate any advice/pointing in the right direction.
So I found the solution was with the configuration of the ASP.Net Core app itself. I performed the following modifications and then everything worked correctly:
Added app.UseForwardedHeaders(); to the request pipeline.
Added the following snippet to service container registration:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
The KnownNetworks and KnownProxies need to be cleared as they default to assuming an IIS hosting environment. For extra security you can add the known proxy/network IPs instead of clearing them here.

Azure AD Redirect URL Using Application Gateway

We have an ASP Core 2.0 App working nicely with Azure AD on the private network. However, we've been playing around with the Azure Application Gateway, investigating the possibility of allowing access to the app from outside for remote workers etc.
We have registered the app on the Gateway, and, once logged in with Azure AD, the anonymous front page is accessible via ourapp.msappproxy.net. However, when signing in (again?) in the app, the client is redirected back to intervalServer/signin-oidc which fails as it is not accessible externally.
While I doubt this is any part of the solution, I have tried overriding the redirect "CallbackPath": "/signin-oidc", to absolute path ourapp.msappproxy.net/signin-oidc but I can't seem to work out how. Changing the reply URL in Azure Portal doesn't help either (although I doubted it would, this is just for verification right?).
I can't seem to find any guidance on this on this particular scenario, so that would be welcome. Otherwise, I'm left pondering the following:
1, If I could change the redirect to ourapp.msappproxy.net/signin-oidc, would that solve the sign in issue?
2, Do I even need an additional sign in step, or should I be changing the app to accept AzureAppProxyUserSessionCookie or AzureAppProxyAccessCookie? (If that's even an option?)
Thanks to rfcdejong in the comments for putting me on track. In our case I was able use Azure AD with the Azure Application Gateway by overriding OnRedirectToIdentityProvider event and supplying the proxy url in ConfigureServices
services.AddAuthentication(...)
.AddOpenIdConnect(options =>
{
options.ClientId = Configuration["Authentication:AzureAD:ClientId"];
options.Authority = Configuration["Authentication:AzureAd:Authority"];
options.CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"];
if (IsProduction) // So that I can use the original redirect to localhost in development
{
Task RedirectToIdentityProvider(RedirectContext ctx)
{
ctx.ProtocolMessage.RedirectUri = "https://ourapp.msappproxy.net/signin-oidc";
return Task.FromResult(0);
}
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = RedirectToIdentityProvider
};
}
})
The return URI needs to be configured to match for the app in Azure Portal.
Users also need to be assigned, but the internal app is now available anywhere without requiring direct access to the server.