ASP.NET Core Reverse Proxy and IdentityOptions Paths - c#

I am running my Kestrel servers behind an IIS Reverse Proxy (.NET Core 1.0.0). It works great, except, certain cases - such as being redirected to a login page upon failed access - results in the user being sent to http://localhost:8080/Account/Login (the address that Kestrel is bound to) instead of http://www.example.net/Account/Login (the reverse proxy address).
I know that I can configure IdentityOptions in Startup.cs, which also includes the ability to set LoginPath, like:
services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Cookies.ApplicationCookie.SlidingExpiration = true;
options.Cookies.ApplicationCookie.LoginPath = PathString("/Admin/Login");
});
Unfortunately, I cannot set an absolute path.
I also looked into the possibility to inspect and rewrite the URL per this response, but I feel like this may not be the right way to go, though I don't have any direct reason as to why.
app.Use(async (context, next) =>
{
var baseUri = Configuration["Site:BaseUri"];
if (baseUri.Contains(context.Request.Host.ToString()))
{
await next();
}
else
{
var newUri = $"{context.Request.Scheme}://{baseUri}{context.Request.Pat‌​h}{context.Request.Q‌​ueryString}";
context.Response.Redirect(newUri);
}
});
On top of all of this, I did find this link from the FunnelWeb project which basically looks like what I am wanting to do (although, for an older version of ASP.NET).
I would appreciate any guidance to point me in the right direction.

Related

Why are .NET 6 Application + Application Gateway + Open ID Connect - Path based routing on different app services not working

I have an application gateway set up ("gateway"):
apps.mydomain.com
I have an app service set up ("app"):
my-app-service.azurewebsites.net
The path based rule is set on the listener for on the gateway address above.
/apps/app1/*
The default backend target and settings are set to the root of the gateway address above.
I am using AADS as the authentication store.
Both work correctly independently as I have another route set up on the gateway. I can go to the app service and it will prompt me for credentials, then take me to the index page at the root.
my-app-service.azurewebsites.net/
What I am trying to do is set up a path based rule that routes through the gateway and lands on a path under apps.mydomain.com. For example,
apps.mydomain.com/apps/app1.
I have set up the gateway properly as I can get to a static page. For example,
apps.mydomain.com/apps/app1/somedirectory/mystaticpage.html.
My problem is that when I try to authenticate, I think the signin-oidc is routing the request incorrectly. I am able to authenticate, and it appears to pass back to apps.mydomain.com/apps/app1/signin-oidc and then the middleware passes back to the root. It is authenticating, because when it hits the error page, it shows me as logged in.
I have tried overriding the cookie policy options:
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.Secure = CookieSecurePolicy.SameAsRequest;
options.MinimumSameSitePolicy = SameSiteMode.None;
//options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.None;
});
I have tried listening to the OnRedirectToIdentityProvider:
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
//options.CallbackPath = new PathString("/apps/app1/");
//options.CallbackPath = new PathString("/apps/app1/signin-oidc");
//options.CallbackPath = "/apps/app1/signin-oidc";
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = (context) =>
{
//https://stackoverflow.com/questions/50262561/correlation-failed-in-net-core-asp-net-identity-openid-connect
context.Options.NonceCookie.Path = "https://apps.mydomain.com/apps/app1/signin-oidc";
context.Options.CorrelationCookie.Path = "https://apps.mydomain.com/apps/app1/signin-oidc";
//https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol#front-door-to-backend
context.ProtocolMessage.RedirectUri = "https://apps.mydomain.com/apps/app1/signin-oidc";
return Task.FromResult(0);
}
};
});
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy.
options.FallbackPolicy = options.DefaultPolicy;
});
My guess is that just setting the otions.CallbackPath should work, but I just get correlation or sorry, we cannot log you in errors when I try that. Not sure if there is an error in the library.
I have spent over a month on and off and engaged MS technical support trying to solve this, but have not been able to get this to work. I can't imagine I am the only one doing this. I know it is in the open ID connect middleware somewhere, but cannot find the correct combination.
This is just a demo project in .NET 6 to get this working correctly. Any code will do. If there is actual working code somewhere that would be great. Just need to get the path based routing with authentication to work.

Microsoft Identity Web: Change Redirect Uri

I am using .net 5, Identity Web Ui to access Microsoft Graph. Where can I configure my Redirect URI?
I need to specify the full Uri, since the generated one from callbackUri is incorrect due to being behind a Load Balancer with SSL offload.
Here is my current ConfigureServices section
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
I was facing a similar problem with a WebApp exposed only behind a front door, the WebApp had to call a custom downstream WebApi.
My service configuration that worked on my localhost dev machine:
// AzureAdB2C
services
.AddMicrosoftIdentityWebAppAuthentication(
Configuration,
"AzureAdB2C", subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
.EnableTokenAcquisitionToCallDownstreamApi(p =>
{
p.RedirectUri = redUri; // NOT WORKING, WHY?
p.EnablePiiLogging = true;
},
[... an array with my needed scopes]
)
.AddInMemoryTokenCaches();
I tried the AddDownstreamWebApi but did not manage to make it work so I just fetched the needed token with ITokenAcquisition and added it to an HttpClient to make my request.
Then I needed AzureAd/B2C login redirect to the uri with the front door url:
https://example.org/signin-oidc and things broke. I solved it like this:
First of all you have to add this url to your App registration in the azure portal, very important is case sensitive it cares about trailing slashes and I suspect having many urls that point to the very same controller and the order of these have some impact, I just removed everything and kept the bare minimum.
Then in the configure services method:
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
// Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State
// that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize
// parameters sent to the identity provider.
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});
With that the redirect worked, but I entered a loop between the protected page and the AzureB2C login.
After a succesful login and a correct redirect to the signin-oidc controller (created by the Identity.Web package) I was correctly redirected again to the page that started all this authorization thing, but there it did not find the authorization. So I added/modded also this:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.Always;
});
With this the authorization worked, but I was not able to get the token to call the downstream API, before this redirect thing ITokenAcquisition worked, but now when trying to get the token it throws an exception.
So in my controller/service to get the token I modified and used:
var accessToken = await _contextAccessor.HttpContext
.GetTokenAsync(OpenIdConnectDefaults.AuthenticationScheme, "access_token");
So now with the token I add it to my HttpRequestMessage like this:
request.Headers.Add("Authorization", $"Bearer {accessToken}");
I lived on StackOverflow and microsoft docs for 3 days, I am not sure this is all "recommended" but this worked for me.
I had the same problem running an asp.net application under Google Cloud Run, which terminates the TLS connection. I was getting the error:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application.
Using fiddler, I examined the request to login.microsoftonline.com and found that the query parameter redirect_uri exactly matched the url I'd configured in the application in Azure except that it started http rather than https.
I initially tried the other answers involving handling the OpenIdConnectEvents event and updating the redirect uri. This fixed the redirect_url parameter in the call to login.microsoftonline.com and it then worked until I added in the graph api. Then I found my site's signin-oidc page would give its own error about the redirect uri not matching. This would then cause it to go into a loop between my site and login.microsoftonline.com repeatedly trying to authenticate until eventually I'd get a login failure.
On further research ASP.net provides middleware to properly handle this scenario. Your SSL load balancer should add the standard header X-Forwarded-Proto with value HTTPS to the request. It should also send the X-Forwarded-For header with the originating IP address which could be useful for debugging, geoip etc.
In your ASP.net application, to configure the middleware:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
Then enable the middleware:
app.UseForwardedHeaders();
Importantly, you must include this before the calls to app.UseAuthentication/app.UseAuthorization that depends on it.
Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0
If your load balancer doesn't add the X-Forwarded-Proto header and can't be configured to do so then the document above outlines other options.
I was facing with similar issue for 3 days. The below code helped me to get out of the issue.
string[] initialScopes = Configuration.GetValue<string>("CallApi:ScopeForAccessToken")?.Split(' ');
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddInMemoryTokenCaches();
services.AddControllers();
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SaveTokens = true; // this saves the token for the downstream api
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctxt =>
{
ctxt.ProtocolMessage.RedirectUri = "https://example.org/signin-oidc";
await Task.Yield();
}
};
});

How to configure the OAuth callback to a different domain in ASP.NET Core authentication

I am Authenticating against an OAuth endpoint where I can only configure 1 callback domain. (and localhost is whitelisted).
I have my web app running in Azure (myapp.azurewebsites.net) and have it available with two custom domains (myapp.cc and myapp.eu). When I use the default setup, the CallbackPath can only be a relative path (to the current domain)
The code documentation of CallbackPath indicates it's relative to the application's base path:
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }
I want to make sure the CallBack happens to the (only) domain that I whitelisted on the OAuth backend. I know I can implement everything manually, but I was hoping there would be an easy way to work around this design and still benefit from the baked in Authentication options.
So even if a user is logging on on the myapp.cc or the myapp.eu or the myapp.azurewebsites.net , it should redirect to myapp.azurewebsites.net/ (which is whitelisted on my Auth service)
A part of my Startup.cs file is pasted below:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", "MyService",
options =>
{
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientOauthSecret;
options.CallbackPath = "/relativeonlypath";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
/// ... removed for brevity
}
);
Any idea on how to implement this?
Thank you
I'm not sure it's possible, because to verify that the user is redirected to your application as part of a "genuine" authentication flow, the ASP.NET OAuth handler performs the following steps:
Before redirecting the user to the OAuth service, ASP.NET Core generates a "correlation" cookie that is tied to the current domain; and
When the user is redirected to the app, the handler looks for this cookie and validates its content.
So if the correlation cookie is generated in step #1 for one domain, let's say myapp.cc, and the user is redirected to another domain, myapp.azurewebsites.net, ASP.NET Core might not be able to read it because the browser will not have included it in the redirection request.
Note
As seen in the first comments, the original thought was to leverage the SameSiteproperty of the correlation cookie to have it sent by the browser to the second domain.
This was all wrong, apologies!
I now think that you have 2 different options:
Redirect every request from myapp.cc and myapp.eu to myapp.azurewebsites.net, so that when the authentication flow happens, we're already on the right domain; or
Redirect the user to the myapp.azurewebsites.net domain before redirecting them to the OAuth server.
I won't go into the first solution, as there's plenty of ways to achieve this.
Here's some code that I haven't tested that could work for the second solution:
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", options =>
{
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
var currentRequestUri = new Uri(context.Request.GetDisplayUrl());
// 1. If we're not on the correct domain, redirect the user to the same page, but on the expected domain.
// The assumption is that the authentication flow will also kick in on the other domain (see 2).
if (currentRequestUri.Host != "myapp.azurewebsites.net")
{
var applicationRedirectUri = new UriBuilder(currentRequestUri)
{
Host = "myapp.azurewebsites.net"
}.Uri.ToString();
context.Response.Redirect(applicationRedirectUri);
return Task.CompletedTask;
}
// 2. If we reach here, it means we're on the right domain, so we can redirect to the OAuth server.
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

Separate DbContext for Identity, User.Identity empty after login

I've got two DBs, one for business entities and one for Identity. I'm also using external (Azure Ad) login to authenticate to Identity. All of this worked when I had it all in one DB and one DbContext. However, as soon as I split it out, the issue I run into is that after logging in, on subsequent requests User.Identity.IsAuthenticated is false (and thus User.Identity.Name is null, as well as Role/Claims data... you get the point). No errors are thrown anywhere in the external registration/login process; it's just as if my application does not know which context to look at for User.Identity information.
Here is the body of my ConfigureServices in Startup.cs:
services.AddAuthentication(sharedOpts =>
{
sharedOpts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOpts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(opts => Configuration.Bind("AzureAd", opts))
.AddCookie();
services.AddIdentity<AspNetUsers, AspNetRoles>()
.AddEntityFrameworkStores<CoreUMContext>()
.AddDefaultTokenProviders();
services.AddMvc();
string dataConnection = Configuration["ConnectionStrings:TrackerDatabase"];
string userConnection = Configuration["ConnectionStrings:UserDatabase"];
Trace.TraceWarning("Connecting to connection at: " + dataConnection);
try {
services.AddDbContext<EdgeContext>(options => options.UseSqlServer(dataConnection));
services.AddDbContext<CoreUMContext>(options => options.UseSqlServer(userConnection));
}
catch (Exception ex) {
Trace.TraceError("Error connecting to DB: " + ex);
}
I know middleware can be tricky with the order you add to the pipeline, so I've tried every combo (.AddIdentity before .AddAuthentication, adding CoreUMContext before the EdgeContext, etc.). Injecting the Identity context (CoreUMContext) into my controllers does not solve the issue either.
Again, this line works
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
and result.Succeeded returns true, so to some degree it's working, but I can't seem to find good literature on the exact path User.Identity follows to resolve itself.
I'd greatly appreciate if anyone can shed light, as I'd really like to not have to go back to an all-in-one-db or all-in-one-context scenario as this doesn't really fit my architectural requirements.
UPDATE
It seems that the issue is Chrome not storing/sending the property cookie Identity uses to resolve which user is making the request. It works in Microsoft Edge however. This switched at some point for me yesterday and I'm still trying to deduce why (clearing cookies has not worked), but it appears the multiple contexts were not the cause of the problem.
It appears this WAS a Chrome-specific issue, namely that Chrome does not agree with the setting of cookies on the domain of "localhost". After recognizing that this bizarrely was not an issue in Incognito, I found this thread: Chrome localhost cookie not being set
which led me to changing my Startup.cs file:
services.AddAuthentication(sharedOpts =>
{
sharedOpts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOpts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(opts => Configuration.Bind("AzureAd", opts))
.AddCookie(opts => {
opts.Cookie.Domain = ""; <-- This made the difference
});
I'm now allowing the browser to resolve the cookie domain instead of taking the default the Identity code was generating. Hopefully this does not prove to be an issue when I publish to production.
What I don't get is why this initially worked, then broke, then stayed broken even after I re-packaged my code all into one context and database; perhaps Chrome allowed a 1-time setting of the cookie but any subsequent changes were ignored.

IdentityServer 4, trying to capture traffic with fiddler?

Console application trying to get discovery
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
Works fine, however i'm trying to figure out how this thing works and I cant seem to capture the http traffic.
if i use http://localhost.fiddler to redirect to the local proxy Errors With:
Error connecting to localhost.fiddler:5000/.well-known/openid-configuration: HTTPS required (it's not setup with HTTPS, the error msg is misleading!)
Strangely later in the code when we try to authenticate to web-api with
var response = await client.GetAsync("http://localhost.fiddler:5001/identity");
localhost.fiddler works fine, now this is running in the same console.app, in program.cs so the same file. This is driving me potty why on earth can't I capture traffic going to 5000 it's HTTP!!! so what mysteries are causing this ? is there another way to view the magic http traffic going to and from Identity Server ?
Added Startup class
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
}
}
added Blog, will update it and credit if we can resolve this.
As you correctly figured out, you need to use, for example, http://localhost.fiddler, to route localhost traffic through fiddler. However, using DiscoveryClient.GetAsync uses DiscoveryClient with default policy. That default policy has the following settings important for this case:
RequireHttps = true
AllowHttpOnLoopback = true
So, it requires https unless you query loopback address. How it knows what is loopback address? There is DiscoveryPolicy.LoopbackAddresses property. By default it contains:
"localhost"
"127.0.0.1"
For that reason you have "HTTPS required" error - "localhost.fiddler" is not considered a loopback address, and default policy requires https for non-loopback addresses.
So to fix, you need to either set RequireHttps to false, or add "localhost.fiddler` to loopback address list:
var discoClient = new DiscoveryClient("http://localhost.fiddler:5000");
discoClient.Policy.LoopbackAddresses.Add("localhost.fiddler");
//discoClient.Policy.RequireHttps = false;
var disco = await discoClient.GetAsync();
If you do this - you will see disovery request in fiddler, however it will fail (response will contain error), because server will report authority as "http://localhost:5000" and you query "http://localhost.fiddler:5000". So you also need to override authority in your policy:
var discoClient = new DiscoveryClient("http://localhost.fiddler:5000");
discoClient.Policy.LoopbackAddresses.Add("localhost.fiddler");
discoClient.Policy.Authority = "http://localhost:5000";
var disco = await discoClient.GetAsync();
Now it will work as expected.

Categories