Swashbuckle using port 80 for https when run behind reverse proxy - asp.net-core

I have a .net core api documented with swagger/swashbuckle.
When running the swagger ui on localhost on url https://localhost:44390/ the "Try it out" works fine.
We have the same solution in an App service in Azure with an Azure Front Door acting as reverse proxy. Front Door only accepts https traffic and only forwards https traffic. Front door domain is widget.example.com and App service is widget-test-app.azurewebsites.net. When running the swagger ui in Azure using the url https://widget.example.com/api/index.html there are two differences compared to running in localhost:
The swagger ui is showing a Servers -heading and a dropdown
The swagger ui is showing the server url as https://widget.example.com:80
I added an endpoint in the api with the following code
return $"Host {HttpContext.Request.Host.Host} Port {HttpContext.Request.Host.Port} Https {HttpContext.Request.IsHttps}";
When requesting https://widget.example.com/api/v1/test/url it returns
Host widget-test-app.azurewebsites.net Port Https True
This is completely ok since Front door is changing the host header. Port is empty, though.
Summary: Swagger ui is showing the correct domain in the Servers -dropdown but the port number is wrong. How can I get it to either omit the port number if it's 80 or 443, or add it correctly?
Update: The problem is in the swagger.json file which behind the reverse proxy includes a servers element
"servers": [{
"url": "https://widget.example.com:80"
}]
Startup.ConfigureServices
services.AddApiVersioning(options => {
options.Conventions.Add(new VersionByNamespaceConvention());
});
services.AddVersionedApiExplorer(o => {
o.GroupNameFormat = "'v'VVV";
o.SubstituteApiVersionInUrl = true;
});
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "Widget backend v1", Version = "v1"
});
c.SwaggerDoc("v2", new OpenApiInfo {
Title = "Widget backend v2", Version = "v2"
});
c.EnableAnnotations();
c.AddEnumsWithValuesFixFilters();
var xmlFile = $ "{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
Startup.Configure
app.UseSwagger(options => {
options.RouteTemplate = "/api/swagger/{documentname}/swagger.json";
});
app.UseSwaggerUI(options => {
foreach(var description in provider.ApiVersionDescriptions) {
options.SwaggerEndpoint($ "/api/swagger/{description.GroupName}/swagger.json", "widget backend " + description.GroupName);
}
options.RoutePrefix = "api";
});

To fix this I cleared the Servers -list. Here is my code:
app.UseSwagger(options =>
{
options.RouteTemplate = "/api/swagger/{documentname}/swagger.json";
options.PreSerializeFilters.Add((swagger, httpReq) =>
{
//Clear servers -element in swagger.json because it got the wrong port when hosted behind reverse proxy
swagger.Servers.Clear();
});
});

The solution (ok, a - mine - solution :)) is to configure forward headers in Startup.
services.Configure<ForwardHeadersOptions>(options =>
{
options.ForwardHeaders = ForwardHeaders.All; // For, Proto and Host
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Doing this, any URL generation in the app (behind reverse proxy) should respect the port-forwarding value. According to documentation known networks should be specified (taken from docs):
Only allow trusted proxies and networks to forward headers. Otherwise, IP spoofing attacks are possible.
See ASP.NET documentation for more details.

Related

Microsoft.AspNetCore.Authentication.OpenIdConnect /oauth2/authorize endpoint form - redirect_uri error

I have an application (.NET 5.0 ASP Net Core) application that I am trying to deploy to an AWS Amazon Linux 2 server. It appears that all aspects of deployment are fine except for authorization with AWS Congnito and Microsoft.AspNetCore.Authentication.OpenIdConnect. Everything works fine in dev/local and the problems only exhibit themselves when in prod deployment.
The issue exhibits itself as an "An error was encountered with the requested page." at https://auth.<mydomain>.com/error?error=redirect_mismatch&client_id=<myclientid> in the Hosted UI when trying to login. I have confirmed and reconfirmed that the Callback URL(s) are set correctly: https://sub.domain.com/signin-oidc, https://localhost:5001/signin-oidc.
My app is running on http://localhost:5000 behind an apache reverse proxy. I suspect that the non-HTTPS portion of the path between Apache and Kestrel is the issue.
What I have noticed is that Microsoft.AspNetCore.Authentication.OpenIdConnect is lacking https in the redirect_uri value that it creates as part of the /oauth2/authorize endpoint it calls.
This is what I see in Dev (no issues):
This is what I see when deployed, note that the redirect_uri is http:
In the App client settings, I can't set the signin-oidc endpoint to use the HTTP.
My ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ResponseType = "code";
options.ResponseType = Configuration["Authentication:Cognito:ResponseType"];
options.MetadataAddress = Configuration["Authentication:Cognito:MetadataAddress"];
options.ClientId = Configuration["Authentication:Cognito:ClientId"];
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "cognito:groups"
};
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = e =>
{
e.ReturnUri = string.Format("/Home/CheckProfile?url={0}", HttpUtility.UrlEncode(e.ReturnUri));
return Task.CompletedTask;
}
};
});
}
So, why is Microsoft.AspNetCore.Authentication.OpenIdConnect using HTTP when it generates the redirect_uri value of the /oauth2/authorize endpoint. Is that somethign that I need to adjust somewhere? And, does that appear to be the core issue that results in my overall https://auth.<mydomain>.com/error?error=redirect_mismatch&client_id=<myclientid> issue?
The core issue here was the reverse proxy; Kestrel running behind Apache. While I had used this setup (with certbot) regularly over the past few years, I had not previously used it with a OIDC auth scheme. The issue was the https termination at apache and the http transmission between Apache and Kestrel. An OIDC auth scheme (in my case supported by AWS Cognito) needs end-to-end https.
The "lacking https in the redirect_uri value that it creates as part of the /oauth2/authorize endpoint" was just the first of many issues I uncovered. I came up with a solution for that issue:
.AddOpenIdConnect(options =>
{
...
options.Events = new OpenIdConnectEvents
{
...
OnRedirectToIdentityProvider = async n =>
{
n.ProtocolMessage.RedirectUri = n.ProtocolMessage.RedirectUri.Replace("http://", "https://");
await Task.FromResult(0);
}
};
});
But this only solved the narrow issue of changing the redirect_uri proto; other cookie SameSite=None/Secure/http issues then appeared.
At this point, I have had success directly exposing Kestrel on 80 and 443. I realize that it's debatable whether this is a prudent idea, but it's working for me at the moment and today (Summer 2021 on .NET 5.0) it seems like Kestrel is maturing to the point where it is not one of those "only do this in development!" tools.
I found both of these articles very helpful:
https://swimburger.net/blog/dotnet/how-to-run-aspnet-core-as-a-service-on-linux
https://thecodeblogger.com/2021/05/07/certificates-and-limits-for-asp-net-core-kestrel-web-server/
Better answer. While the "Kestrel exposed to the world" answer worked, I ended up figuring out how to make the reverse proxy work with Cognito.
In the reverse proxy I ended up setting "'https' env=HTTPS" as shown here:
<VirtualHost *:*>
RequestHeader set "X-Forwarded-Proto" 'https' env=HTTPS
</VirtualHost>
I also rearanged my Prod Configure(...) as follows:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseForwardedHeaders();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseForwardedHeaders();
app.Use((ctx, next) =>
{
ctx.Request.Host = new HostString("sub.domain.com");
ctx.Request.Scheme = "https";
return next();
});
app.UseHsts();
}

SignalR .Net client cannot connect with https

I am using SignalR Core 2.4.1.0.
This is an Owin project, self-hosted.
My configuration:
public void Configuration(IAppBuilder app)
{
app.Use(async (ctx, next) =>
{
Console.WriteLine($"Incoming: {ctx.Request.Path}");
await next();
});
app.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration();
GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(10);
hubConfiguration.EnableDetailedErrors = true;
app.MapSignalR(hubConfiguration);
}
I am able to connect to https://localhost:9999/signalr/hubs from a web browser fine.
I am also able to connect to SignalR when not using https. (after removing the urlacl )
I also have tried adding a middleware before the SignalR to see the incoming request.
With http the middleware shows the Request and path.
With https the middleware shows the Request from the web browser but never shows any request from the client.
The client just changes states from connecting to disconnected with not exceptions.
My client for testing is .Net console application:
var hub = new HubConnection("https://localhost:9999");
var hubProxy = hub.CreateHubProxy("MyHUB");
hub.Error += (e) =>
{
};
hub.StateChanged += (s) =>
{
Console.WriteLine(s.NewState.ToString());
};
hub.Start();
Console.ReadLine();
I've used SignalR before but this is my first time trying to implement ssl.
In summary, .Net client will connect via http but not https.
Browser can connect to the JS library over https but I haven't tried using the JS library yet.
T.I.A.

Set OpenID Connect CallbackPath to HTTPS

I have a .Net Core 3.1 application that is hosted in AWS behind an https load balancer. To the outside world it is an https site, but to AWS internally it runs on http behind the balancer.
Because of this the OpenID Connect middleware is redirecting to the HTTP path instead of HTTPS.
Is there anyway to force OpenId Connect to use https pathing?
.AddOpenIdConnect("oidc", options =>
{
var oauthConfig = Configuration.GetSection("OAuthConfiguration").Get<OAuthConfiguration>();
options.Authority = oauthConfig.Authority;
options.ClientId = oauthConfig.ClientId;
options.ClientSecret = oauthConfig.ClientSecret;
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.CallbackPath = "/signin-oidc";
When the authorization request is made this generates this redirect uri
"RedirectUri": "http://demo.mysite.com/signin-oidc"
I cannot hardcore a path into the CallbackPath because my application runs multitenancy and the URL is different depending upon routing.
You can force the provider to rewrite your callback url in https like this
option.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri);
builder.Scheme = "https";
builder.Port = -1;
context.ProtocolMessage.RedirectUri = builder.ToString();
return Task.FromResult(0);
}
}
The redirect URI should be an HTTPS value:
Login response returned to https://web.mycompany.com/myapp?code=a0wfd78
Load balancer routes the response to http://server1/myapp?code=a0wfd78
In terms of multitenancy I would try to avoid interfering with the Open Id Connect login process and instead use the same callback path for all users. That is the standard behaviour, and using things like wildcards in redirect URIs can create security vulnerabilities.
Not sure I fully understand understand your requirements related to multitenancy, so if this doesn't work for you, please post some further details on how you want it to work.
.Net Core has events you can override, such as this one, if the redirect URI needs to be calculated at runtime:
options.Events.OnRedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.RedirectUri = <load balanced value>;
await Task.FromResult(0);
}
Solution
The answer here is to insert middleware that pretends all requests came in as HTTPS. Works for me.
ASP.NET 5 OAuth redirect URI not using HTTPS
Warning
The following doesn't work. When deployed it causes a "Correlation failed" error, presumably because the URL was tampered with.
This is the complete fix for my website. Note that I'm loading my options from config.
.AddGoogle(options =>
{
this.Configuration.Bind("OAuth2:Providers:Google", options);
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub", "string");
options.Events.OnRedirectToAuthorizationEndpoint = MakeHttps;
})
Importantly, this event actually performs the redirect, else you'll get a 200 OK and a blank page.
private Task MakeHttps(RedirectContext<OAuthOptions> arg)
{
// When behind a load balancer the redirect URL, which is configured as CallbackPath in the appsettings.json
// is created as HTTP because the HTTPS request is terminated at the NLB and is forwarded in clear text.
// The policy of most OAuth IDPs is to disallow clear HTTP redirect URLs.
if (!arg.RedirectUri.Contains("redirect_uri=https", StringComparison.OrdinalIgnoreCase))
{
arg.RedirectUri = arg.RedirectUri.Replace("redirect_uri=http","redirect_uri=https", StringComparison.OrdinalIgnoreCase);
}
arg.HttpContext.Response.Redirect(arg.RedirectUri);
return Task.CompletedTask;
}

Infinite auth loop when using RunProxy and OpenIdConnect

Before getting to the question - which is how do we solve the infinite authentication loop - some information regarding architecture.
We are using .net core 2.1.
We have 2 services. The first one is the one that's facing the public traffic, does the TLS termination and figures out if the request should be passed on or not. (Perhaps to other servers) When this server figures out that the request is made to a certain path, it uses RunProxy method to map the request to the 'other' service using http. That code looks like below:
app.MapWhen(<MatchRequestCondition>, proxyTime => proxyTime.RunProxy(
new ProxyOptions
{
Scheme = "http",
Host = "localhost",
Port = "1122"
}
));
As an example, if you visit https://localhost:1234/abc - this would be mapped to http://localhost:1122 - which is the port where the second application lives.
Now, this secondary service uses OpenIdConnect - the configuration of it looks like below.
// Configure Services method
services.AddMvc(mvcOptions => {
AuthorizationPolicy policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(auth =>
{
auth.ClientId = "<client_id>";
auth.ClientSecret = "<client_secret>";
auth.Authority = "<authority>";
});
// Configure method
app.UseAuthentication();
Here's where it gets interesting:
If I visit the second node (the one that's meant to receive traffic from the first one only) directly - like http://localhost:1122 - I'm redirected to sign-in and everything works correctly.
But if I visit the first node (which is the one that the real traffic should be coming from) - it goes into a crazy authentication loop.
Any ideas to what might be the root cause? How is this different than having a load balancer in front of the regular service? Or perhaps it's because I'm using the cookie middleware in the secondary service?

.Net core2 CORS - How SetIsOriginAllowed works?

Want to allow my API to be accessed from different sites. For this had:
services
.AddCors(options =>
{
options.AddPolicy(PolicyName, builder =>
{
builder
.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins(
"http://*.my-api.com",
"http://*.my-api.service"
)
...
This doesn't seem to allow httpS or when I specify the port in the request.
Ex.:
https://www.my-api.com:3000
Thought could replace the WithOrigins with SetIsOriginAllowed()
services
.AddCors(options =>
{
options.AddPolicy(PolicyName, builder =>
{
builder
.SetIsOriginAllowed(IsOriginAllowed)
where IsOriginAllowed function is defined as:
private static bool IsOriginAllowed(string host)
{
var corsOriginAllowed = new[] { "my-api.com", "my-api.service" };
return corsOriginAllowed.Any(origin =>
Regex.IsMatch(host, $#"^http(s)?://.*{origin}(:[0-9]+)?$", RegexOptions.IgnoreCase));
}
but this doesn't work at all, even the regular expression is returning true when I want.
Does anyone know why this doesn't work and can show me the right way to allow httpS (besides duplicating all the domains in WithOrigins() with httpS and different ports.
Thanks
SetIsOriginAllowed() does work. Was testing with Postman and as was told, Postman doesn't care about headers returned from the server. It's the browser who enforces the Cors headers.
To test properly created a little html page under a test site with below javascript
<html>
<script>
fetch('http://test.com:5000/v2/campaign/hallo3').then(function(response) {
return response.json();
}).then(function(j) {
alert(JSON.stringify(j));
});
</script>
</html>
when domain is NOT included in the Cors allowed list browser doesn't display the returned values from API
After adding test domain to allowed domains list browser display the data and get the content Cors headers
Another problem was that with just the SetIsOriginAllowed() server was not sending the 'Vary' header. Had to set both:
.SetIsOriginAllowed(IsOriginAllowed)
.WithOrigins(corsOriginAllowed)
23/12/2022
For anyone struggling with this in NET CORE 7 try this on Program.cs:
Add the variable:
...
var MyHosts = "myHosts";
var builder = WebApplication.CreateBuilder(args);
...
Add the new CORS policy:
if (builder.Environment.IsDevelopment())
{
///Add a CORS policy to allow certain hosts
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyHosts,
policy =>
{
policy.AllowAnyOrigin().WithOrigins("http://localhost:59028").AllowAnyHeader().AllowAnyMethod();
});
});
}
U have to add the AllowAnyHeader and AllowAnyMethod or u'll get another pre-flight error.
Don't forget to add the new policy below:
app.UseCors(MyHosts);