Set OpenID Connect CallbackPath to HTTPS - asp.net-core

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;
}

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();
}

How to set callback host (not path) for .NET Core Open ID Connect?

In .NET Core, using Open ID Connect, I'm trying to debug locally behind a local Nginx instance.
Nginx and hosts file settings are used so that a custom domain e.g. https://example.com is used to access the web app running locally, rather than the usual https://localhost:<port>
The issue I have, is that the identifty provider, Okta, always calls back to https://localhost:<port>/authorization-code/callback rather than https://example.com/authorization-code/callback. Both https://localhost:<port> and https://example.com are included in allowed callback URIs in Okta.
The .NET Open ID Connect library appears to discover the callback host automatically and I can't find a setting to change it, only the path.
.AddOpenIdConnect(options =>
{
options.ClientId = Configuration["Okta:ClientId"];
options.ClientSecret = Configuration["Okta:ClientSecret"];
options.Authority = Configuration["Okta:Issuer"];
options.CallbackPath = "/authorization-code/callback";
options.ResponseType = "code";
options.SaveTokens = true;
options.UseTokenLifetime = false;
options.GetClaimsFromUserInfoEndpoint = true;
...
Is there a setting I can use to forward the host for the callback?

Swashbuckle using port 80 for https when run behind reverse proxy

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.

AddAuthentication with AddCookie Redirects to http instead of https

We have an ASP.Net Core website and use AddAuthentication with AddCookie to redirect the user to the OAuth login provider when they are not logged in. We can provide a relative path to the login action with LoginPath. This has to be a relative path; a full path results in an exception. The problem is that our website is behind a load balancer. The traffic to the load balancer is https, but from there it becomes http to the web server. This causes the redirect url to be http, instead of https. How can we control the LoginPath to use https as the redirect?
I found the answer at the post recommended by #GabrielLuci.
ASP.NET Core CookieAuthenticationOptions.LoginPath on different domain
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.Name = "myCookie";
o.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = (context) =>
{
context.HttpContext.Response.Redirect("https://externaldomain.com/login");
return Task.CompletedTask;
}
};
});

JWT Authentication

My team is building a web api in .net core. We use token authentication to authenticate any client, the token comes form the Azure AD.
We put this code in Startup.cs
app.UseJwtBearerAuthentication(options => {
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.Authority = "https://login.windows.net/1da3434a9-e5a6-4e9e-80c3-aca8ed37cb03";
options.Audience = "https://test.onmicrosoft.com/test_api";
options.RequireHttpsMetadata = false;
});
services.AddCors(options =>
{
// Define one or more CORS policies
options.AddPolicy("AllowAll",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
This one only work when we allow anonymous access. I checked the token, and the token is valid. But every time we hit controller it will throw cors error. even though we enable cors
The sequence of adding middleware does matter!
To enable CORS for your entire application add the CORS middleware to
your request pipeline using the UseCors extension method. Note that
the CORS middleware must proceed any defined endpoints in your app
that you want to support cross-origin requests (ex. before any call to
UseMvc).
You should put services.AddCors above almost everything that might redirect your users via 401.
You cannot combine AllowCredentials & AllowAnyOrigin. Pick a specific origin and your request will likely work.