0

Environment
I have a special case with a single ASP.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 authorizating what user has access to what domain.

For example, all these domains go to the same ASP.Core web application, and many more:

  • device1.mydomain.io
  • device2.mydomain.io
  • device3.mydomain.io
  • deviceN.mydomain.io
  • anything.mydomain.io

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 webserver 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.

  • The user has access to domain1.mydomain.io, but no access to domain2.mydomain.io.
  • The user logs into domain1.mydomain.io and ASP.Core service signs a cookie.
  • The user copies the cookie into Postman and uses it to contact domain2.mydomain.io.
  • Now the user has access to domain2.mydomain.io too, since the ASP.Core service never checks which domain the cookie was signed for.

How can I make the ASP.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.

Multiple & SubDomain's cookie in asp.net Core Identity

  • 1
    setting `CookieDomain` should work but the problem is how you set that info for each app, maybe involving something dynamic. – King King Feb 24 '21 at 10:49
  • Could do that by implementing a custom Cookie Manager, but I think that it only tells the browser which domain to use that cookie for. And doesn't prevent authenticating a forged cookie signed for a different domain. – Christian Lundheim Feb 24 '21 at 11:00
  • 1
    have you tried it yet? I mean the cookie authentication should use that info for verifying, in a scenario of multiple separate applications, one thing you need do is set that to one same value, so that the user logged-in one application will not have to log-in the other (something like SSO). I've done a hands-on practice with that and pretty sure about that. – King King Feb 24 '21 at 11:12
  • Cool, I guess I may have assumed too much without trying. I will try now. – Christian Lundheim Feb 24 '21 at 11:18
  • 1
    however it's still strange that, by default if not setting the `CookieDomain`, it's considered as being *exactly* matched with the current host name, so suppose after logging in `domain1.mydomain.io` , the cookie then should not be possible to be used to log-in `domain2.mydomain.io`. What about the `dataProtectionProvider`? that custom data protection provider may matter. – King King Feb 24 '21 at 11:19
  • I just removed the custom data protection provider, it does not seem to have any effect. Seems the data protection provider is only encrypting/decrypting the data, it has no meaning of the context the data was encrypted/decrypted in (such as the hostname). But it's good to have it out of the equation. I also tried to explicitly setting the Cookie's domain in a custom cookie manager, like the linked issue, however, like you suggest, it had no effect. Seems the default implementation is similar to what my custom cookie manager does now. – Christian Lundheim Feb 24 '21 at 11:36

1 Answers1

0

I found a solution!

It seems like ASP.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 webserver can always accept cookies just based on that the same webserver 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. :)

  • if possible, we may validate the info sooner to improve the performance a bit but intercepting at this point is OK. I've just referenced the source code for `CookieAuthenticationHandler` here https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs and looks like there is no check for cookie domain, really weird. Actually that info if any should be set in the cookie, but this `redirectUri` in your code does not seem to be extracted from cookie? – King King Feb 24 '21 at 14:25
  • Actually we have many extensibility points so that you can add your custom logic for authentication (any points before the mvc middleware and all points inside the filters) . However what we want (to keep it as clean as possible) is try to take advantage of any built-in code or at least stick to the cookie authentication handler. – King King Feb 24 '21 at 14:27
  • This current solution does take advantage of the cookie authentication handler. It just adds logic for the domain validation. I'm not sure performance would increase by checking it earlier because roughly 100% of these checks will succeed. So code would have to run anyway. Check only fails if someone attempt to hack it by using a wrong cookie signed for another server. – Christian Lundheim Mar 04 '21 at 16:17
  • The redirectUri is extracted from the cookie. It comes from the decrypted JWT token. I've manually checked it by trying to abuse my cookie for the server on a different domain, and it returned the redirectUri it was originally signed for. – Christian Lundheim Mar 04 '21 at 16:21