Environment
I have a special case with a single ASP.NET Core 5 web application hosted on a wildcard domain.
I have an infinite number of dynamic sub-domains, and there is a Single-Sign-On OpenID authority responsible for authentication and authorising what user has access to what domain.
For example, all these domains go to the same ASP.NET Core web application, and many more:
The Single-Sign-On server will refuse to sign your login if the return URL during the OIDC-redirect points to a sub-domain that your user should not have access to. Either you have access to that particular sub-domain or you do not.
Considerations so far
So far, we have added event handlers to the OpenID cycle of the web server to dynamically pick an OIDC Client ID based on the URL we were contacted on before the redirect to Single-Sign-On server.
After the redirect, this application will also refuse to accept the token signed by the Single-Sign-On server if it was signed for a different redirect URL than this application was contacted on. This to prevent someone from copying the token, and changing the URL and trying to use the same token for a different sub-domain the user should not have access to.
There are no longer any security problems that I can see in the OpenID redirect-cycle itself. And all here is working fine.
Problem
However now there is a security problem after the cookie has been signed when using the service.
domain1.mydomain.io
, but no access to domain2.mydomain.io
.domain1.mydomain.io
and ASP.NET Core service signs a cookie.domain2.mydomain.io
.domain2.mydomain.io
too, since the ASP.NET Core service never checks which domain the cookie was signed for.How can I make the ASP.NET Core cookie-authentication middleware check which domain the cookie was signed for, and refuse it if the domain differs from the one we were contacted at?
The Startup.cs
code:
void AddOpenIdConnectServices(IServiceCollection services, IDataProtectionProvider dataProtectionProvider)
{
services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(o =>
{
o.DataProtectionProvider = dataProtectionProvider;
o.Cookie.SameSite = SameSiteMode.None;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = this.config.OpenId_Authority;
options.ClientId = this.config.OpenId_ClientId;
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
// Ensure that the "state" sent to the SSO server is encrypted with the same secret as the other webservers use when scaled to >1.
// Without this login will fail because we're unable to decrypt the "state" at "signin-oidc" endpoint when coming back from the SSO-server.
options.DataProtectionProvider = dataProtectionProvider;
// Customize OpenID so that we can provide the SSO- server a dynamic client-id based on which hostname we were contacted on.
// We need to intercept the redirection to the SSO- server, as well as the audience/client-id validation when the JWT- token is returned from the SSO- server.
DynamicOpenIdClientHandler dynamicClientId = new DynamicOpenIdClientHandler(clientIdPrefix: options.ClientId);
options.Events.OnRedirectToIdentityProvider = dynamicClientId.OnRedirectToidentityProvider;
options.TokenValidationParameters.AudienceValidator = dynamicClientId.AudienceValidator;
options.Events.OnTokenValidated = dynamicClientId.OnTokenValidated;
});
}
Similar questions
This is a similar question, however I'm not sure customizing the cookie manager is the best way to go, or if it solves the problem.
I found a solution!
It seems like ASP.NET Core cookie authentication by default does not care about the hostname the cookie was signed for when the token is validated on each request. And probably for a good reason. In most use cases the web server can always accept cookies just based on that the same web server signed it, and not care about how we were contacted.
This behaviour can be changed by adding additional principal validation to Events.OnValidatePrincipal
when configuring AddCookie
during startup.
I added an extra check validating the hostname the cookie was signed for, with the current actual hostname. This works, the server no longer accepts cookies signed for the wrong hostname. It will now redirect these requests to the Single-Sign-On server instead.
o.Events.OnValidatePrincipal = context =>
{
if (context.Properties.Items.TryGetValue("OpenIdConnect.Code.RedirectUri", out string redirectUri))
{
Uri cookieWasSignedForUri = new Uri(redirectUri);
if (context.Request.Host.Host != cookieWasSignedForUri.Host)
{
context.RejectPrincipal();
}
}
return Task.CompletedTask;
};
I feel this solution is pretty safe and straight forward once found. If anyone see later and know a better solution, please tell me. :)