aspnet / AspNetKatana

Microsoft's OWIN implementation, the Katana project
Apache License 2.0
960 stars 331 forks source link

Cookie compatibility between OWIN and Microsoft.AspNetCore.Authentication #435

Closed Schoof-T closed 2 years ago

Schoof-T commented 2 years ago

Hello

I have applications both running on ASP.NET MVC (4.7.2) and ASP.NET Core (Blazor, 5.0). I secure these with OpenIdConnect. I would like the cookie to be compatible between these applications, as it currently stands these both create their own cookie and requires re-authentication.

Example: I connect to portal.domain.com (ASP.NET MVC 4.7.2), this requires an authentication flow and creates a cookie. I click through to application.portal.domain.com (ASP.NET Core (Blazor, 5.0) and this requires another authentication flow and creates a second cookie. However if I click through to application2.portal.domain.com (ASP.NET MVC 4.7.2) (from portal.domain.com) this does not require a second authentication flow and re-uses the original cookie (as it should).

Are there specific settings that need to be adjusted so these cookies become compatible?

ASP.NET Authentication Code (Startup file)

            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
                CookieSameSite = SameSiteMode.Lax,
#if !DEBUG
                CookieDomain = ConfigurationManager.AppSettings["OpenIdCookieDomain"]
#endif
            });

            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
            {
                ClientId = ConfigurationManager.AppSettings["OpenIdClientId"],
                ClientSecret = ConfigurationManager.AppSettings["OpenIdClientSecret"],
                SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
#if DEBUG
                RequireHttpsMetadata = false,
#endif
                Scope = "openid profile email",
                Authority = ConfigurationManager.AppSettings["OpenIdAuthority"],
                MetadataAddress = ConfigurationManager.AppSettings["OpenIdMetadata"],
                RedirectUri = ConfigurationManager.AppSettings["OpenIdRedirect"],
                ResponseType = OpenIdConnectResponseType.Code,
                RedeemCode = true,
                UsePkce = true,
            });

ASP.NET Core Authentication Code (Startup file)

 services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
               .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
               {
#if !DEBUG
                   options.Cookie.Domain = Configuration["Security:CookieDomain"];
#endif
               })
               .AddOpenIdConnect("oidc", options =>
               {
                   options.Authority = Configuration["Security:Authority"];
                   options.MetadataAddress = Configuration["Security:MetadataAddress"];
                   options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                   options.ClientId = Configuration["Security:ClientId"];
                   options.ClientSecret = Configuration["Security:ClientSecret"];
                   options.ResponseType = Configuration["Security:ResponseType"];
                   options.SaveTokens = true;
                   options.GetClaimsFromUserInfoEndpoint = true;
                   options.UseTokenLifetime = false;
                   options.Scope.Add("openid");
                   options.Scope.Add("profile");
                   options.Scope.Add("email");
                   options.UsePkce = true;
               });

Thanks in advance

Tratcher commented 2 years ago

See https://github.com/dotnet/AspNetCore.Docs/issues/21987

Schoof-T commented 2 years ago

See dotnet/AspNetCore.Docs#21987

Thank you for the quick reply. I'm guessing my google skills weren't up to snuff. I managed to get this working, but with a couple of issues.

Using the code listed for my .NET 4.x websites, this creates a very large cookie. Up to the point where I can no longer add any custom claims or I get the famed "HTTP Error 400. The size of the request headers is too long." exception. I'm trying to add about 50 claims, totaling up to about 80 claims. (I don't know if that is a lot, but it doesn't feel like it). The other claims we get from our identity provider.

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
                CookieName = ".AspNet.SharedCookie",
                CookieSameSite = SameSiteMode.Lax,
                SlidingExpiration = true,
                ExpireTimeSpan = TimeSpan.FromMinutes(120),
                TicketDataFormat = new AspNetTicketDataFormat(
                    new DataProtectorShim(
                        DataProtectionProvider.Create(new DirectoryInfo(@"D:\\KeyDirectory"),
                        (builder) =>
                        {
                            builder.SetApplicationName("SharedCookieApp");
                        })
                        .CreateProtector(
                            "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                            "Cookies",
                            "v2"))),
                CookieManager = new Microsoft.Owin.Security.Interop.ChunkingCookieManager()
#if !DEBUG
                CookieDomain = ConfigurationManager.AppSettings["OpenIdCookieDomain"]
#endif
            });

            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
            {
                ClientId = ConfigurationManager.AppSettings["OpenIdClientId"],
                ClientSecret = ConfigurationManager.AppSettings["OpenIdClientSecret"],
                Authority = ConfigurationManager.AppSettings["OpenIdAuthority"],
                MetadataAddress = ConfigurationManager.AppSettings["OpenIdMetadata"],
                SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
                RedirectUri = ConfigurationManager.AppSettings["OpenIdRedirect"],
#if DEBUG
                RequireHttpsMetadata = false,
#endif
                Scope = "openid profile email",
                ResponseType = OpenIdConnectResponseType.Code,
                SaveTokens = true,
                UseTokenLifetime = false,
                RedeemCode = true,
                UsePkce = true,
            });

image

Adding the code to my .NET 5 application does not generate a cookie as big (still quite large).

            services.AddDataProtection()
                .PersistKeysToFileSystem(new DirectoryInfo(@"D:\\KeyDirectory"))
                .SetApplicationName("SharedCookieApp");

            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
               .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
               {
                   options.Cookie.Name = ".AspNet.SharedCookie";
                   options.Cookie.SameSite = SameSiteMode.Lax;
                   options.Cookie.Path = "/";
                   options.Cookie.HttpOnly = true;
                   options.Cookie.IsEssential = true;
                   options.ExpireTimeSpan = TimeSpan.FromMinutes(120);
#if !DEBUG
                   options.Cookie.Domain = Configuration["Security:CookieDomain"];
#endif
               })
               .AddOpenIdConnect("oidc", options =>
               {
                   options.ClientId = Configuration["Security:ClientId"];
                   options.ClientSecret = Configuration["Security:ClientSecret"];
                   options.Authority = Configuration["Security:Authority"];
                   options.MetadataAddress = Configuration["Security:MetadataAddress"];
                   options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                   options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                   options.GetClaimsFromUserInfoEndpoint = true;

                   options.Scope.Add("openid");
                   options.Scope.Add("profile");
                   options.Scope.Add("email");
                   options.ResponseType = OpenIdConnectResponseType.Code;
                   options.SaveTokens = true;
                   options.UseTokenLifetime = false;
                   options.UsePkce = true;
               });

image

Is there any way to decrease the size of the cookies? I have noticed they have dramatically increased after we switched to using PCKE.

A second thing I've noticed is that adding the Nuget Microsoft.Owin.Security.Interop also install a huge amount of extra nuget packages. Is this really necessary? I don't seem to be using any of them in my Startup file. image

Tratcher commented 2 years ago

50-80 claims is a lot to try to fit into a cookie.

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/additional-claims?view=aspnetcore-6.0#map-user-data-keys-and-create-claims

If a large amount of user data is required for processing user requests:

  • Limit the number and size of user claims for request processing to only what the app requires.
  • Use a custom ITicketStore for the Cookie Authentication Middleware's SessionStore to store identity across requests. Preserve large quantities of identity information on the server while only sending a small session identifier key to the client.

https://github.com/dotnet/aspnetcore/blob/2af03e53f1fd6cb48c5dba9440064ccdb1060657/src/Security/Authentication/Cookies/src/CookieAuthenticationOptions.cs#L135

A second thing I've noticed is that adding the Nuget Microsoft.Owin.Security.Interop also install a huge amount of extra nuget packages. Is this really necessary? I don't seem to be using any of them in my Startup file.

Unfortunately those are required, they're all referenced indirectly from the interop code you're adding.

Schoof-T commented 2 years ago

I couldn't find a working example for ITicketStore for ASP.NET and ASP.NET Core so I've opted to store my access rights in Session (HttpContext.Current.Session) for my ASP.NET applications and in MemoryCache (Microsoft.Extensions.Caching.Memory) for my Blazor applications. Does this seem like a good approach?

Unfortunately those are required, they're all referenced indirectly from the interop code you're adding.

Would you recommend updating them to the latest version or keeping them on a specific version? I'm asking because a lot of these nugets seem to be updated for .NET 6 and I'm thinking the Microsoft.Owin.Security.Interop was created with a specific version.

Tratcher commented 2 years ago

Don't use Session for auth data, it doesn't have the same lifetime and could leak after signout. You can use MemoryCache in both apps.

You can update to the latest patch version, but don't update to 6.0, there are breaking changes across major versions.

Schoof-T commented 2 years ago

Don't use Session for auth data, it doesn't have the same lifetime and could leak after signout. You can use MemoryCache in both apps.

Okay thanks! Any suggestions for the lifespan settings of the MemoryCache? (I have load balanced websites as well, does that have any impact?)

Current settings are as follows

 var cacheExpiryOptions = new MemoryCacheEntryOptions
                {
                    AbsoluteExpiration = DateTime.Now.AddMinutes(60),
                    Priority = CacheItemPriority.High,
                    SlidingExpiration = TimeSpan.FromMinutes(5)
                };

You can update to the latest patch version, but don't update to 6.0, there are breaking changes across major versions.

What version would you suggest upgrading to? 2.x,, 3.x, 5.x, 6.x? Considering I am on .NET Framework 4.7.2

Tratcher commented 2 years ago

Oh, for load balanced sites you'd need to use IDistributedCache instead, and that will have its own lifetime settings. It's mainly a tradeoff of how much cache space you're willing to use vs how long you're willing to let people sit idle and stay signed in.

Stick with 2.1 if you're on .NET Framework, I think that's the last supported version of Microsoft.Owin.Security.Interop and you want the dependencies to be at the same major version.

Schoof-T commented 2 years ago

So just to confirm, I have these nuggets at the following versions (not all had a 2.1 release).

Does that seem okay?

Tratcher commented 2 years ago

I think you'll want the 4.x versions for the System dependencies, but if it works then I wouldn't sweat it too much.

Schoof-T commented 2 years ago

Thanks again! I have another follow-up question.

I figured out why my cookie is so large and it's because I have set options.SaveTokens = true;. Does not using this option have a negative effect? What is the alternative?

Tratcher commented 2 years ago

You only need SaveTokens if you're going to make API calls on behalf of the user using those tokens. Even then, you can store those tokens in a local user database, they don't need to be in the cookie.

Schoof-T commented 2 years ago

You only need SaveTokens if you're going to make API calls on behalf of the user using those tokens. Even then, you can store those tokens in a local user database, they don't need to be in the cookie.

Excellent, thanks a lot for all the help!

Schoof-T commented 2 years ago

@Tratcher I'm sorry for re-opening this, but after having disabled SaveTokens our Logout does not work any more. I get the exception "The endSession endpoint requires an id_token_hint parameter".

Logout Methods

HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
HttpContext.GetOwinContext().Authentication.SignOut("oidc");

When I try to get the IdToken now it is indeed null. When setting SaveTokens=true it does indeed return the token once again.

var idToken = ...GetTokenAsync(OpenIdConnectParameterNames.IdToken).Result;

I'm assuming I will have to save the IdToken elsewhere to be able to handle this (like in the database as you said), or is there another way around this? This IdToken is quite large. Can you set the IdToken Manually in the SignOut method? Where would I get the id_token from to store it somewhere?

Tratcher commented 2 years ago

The default signout code doesn't set id_token_hint, where were you setting it? https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L80-L84

Schoof-T commented 2 years ago

So I figured out this works with our ASP.NET (OWIN) Applications but not our ASP.NET Core Applications. I'm guessing they have a different implementation?

Tratcher commented 2 years ago

Ah, AspNetCore does set the hint, though that shouldn't matter if the token isn't stored. https://github.com/dotnet/aspnetcore/blob/a4fe58d890724b46b9d8aa7d3a135ca304cc26d6/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L235

Do you have a trace of the signout request? Is id_token_hint present but empty? Otherwise I don't know why the remote provider would complain about the missing hint for core and not katana.

Schoof-T commented 2 years ago

This is the signout request url? .../auth/oauth2/connect/endSession?post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44320%2Fsignout-callback-oidc&state=CfDJ8GqjlgHph8lMhb0irgIJZtUMEBBFfYfYOfD7C98D0LuQqvrMiwO2jfCheEMFj2p9RBLFfk0IUN6QRQ14b938uP-CiKheQPAIaYevgvlOBw5Vrc7HNoGLzCNtX4PitbfPNQ873m1ZukqTXOl0BMCcqhZqxXq9aLAI1PItYyISR1BihCu-T_ntsJK-EIpXeD7ycQ&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=6.7.1.0

We are using ForgeRock and in their documentation the id_token_hint is required apparently.

I do wonder why it does work in OWIN, it might be because after the signout we do a redirect.


public class AccountController : Controller
    {
        [Authorize]
        public void Logout()
        {
            HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
            HttpContext.GetOwinContext().Authentication.SignOut("oidc");
            HttpContext.Response.Redirect(ConfigurationManager.AppSettings["OpenIdRedirectPostLogout"]);
        }
    }
Tratcher commented 2 years ago

Oh, yeah, you're skipping the OIDC remote logout there, you're only logging out locally.

Schoof-T commented 2 years ago

Oh, yeah, you're skipping the OIDC remote logout there, you're only logging out locally.

What would be the correct way to logout fully there, or is that not an issue? Maybe that could be a solution for the ASP.NET Core Application as well?

Tratcher commented 2 years ago

Instead of adding your own redirect to OpenIdRedirectPostLogout, you pass that value to Signout in the AuthenticationProperties.RedirectUri. See https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L89-L93 You can also set it on the options. https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L94-L97

That said, you'll be back in the same position of needing the id token hint, storing the token in a database, and adding it to the signout in the events.

Schoof-T commented 2 years ago

Thanks! That makes sense. How do I add the id_token_hint to the signout? And how do I get the id_token_hint to starting storing it? I know SaveTokens does it automatically, but I can't seem to find how to do it manually.

Tratcher commented 2 years ago

You'd need to hook into the events and update the OpenIdConnectMessage.

See https://github.com/dotnet/aspnetcore/blob/5b742abe2fad7e19bb4ace67ef5fd5d20cff0dd3/src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs#L39 https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs#L48

Storing and retrieving the token elsewhere is up to you.

Schoof-T commented 2 years ago

Instead of adding your own redirect to OpenIdRedirectPostLogout, you pass that value to Signout in the AuthenticationProperties.RedirectUri. See

https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L89-L93

You can also set it on the options.

https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L94-L97

That said, you'll be back in the same position of needing the id token hint, storing the token in a database, and adding it to the signout in the events.

I've tried to do this in the following ways but without success:

 public class AccountController : Controller
    {
        [Authorize]
        public void Logout()
        {
            HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
            HttpContext.GetOwinContext().Authentication.SignOut(new AuthenticationProperties { RedirectUri = ConfigurationManager.AppSettings["OpenIdRedirectPostLogout"] }, "oidc");
        }
    }
 app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
            {
                ClientId = ConfigurationManager.AppSettings["OpenIdClientId"],
                ClientSecret = ConfigurationManager.AppSettings["OpenIdClientSecret"],
                Authority = ConfigurationManager.AppSettings["OpenIdAuthority"],
                MetadataAddress = ConfigurationManager.AppSettings["OpenIdMetadata"],
                SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
                PostLogoutRedirectUri = ConfigurationManager.AppSettings["OpenIdRedirectPostLogout"],
...

When I send the user to the page Account/Logout I just get a blank page.

You'd need to hook into the events and update the OpenIdConnectMessage.

See https://github.com/dotnet/aspnetcore/blob/5b742abe2fad7e19bb4ace67ef5fd5d20cff0dd3/src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs#L39

https://github.com/aspnet/AspNetKatana/blob/b32c437b05217f367c201e420f147acf115e0712/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationNotifications.cs#L48

Storing and retrieving the token elsewhere is up to you.

This is also not the case for ASP.NET (Owin) applications right? That does not have an OnRedirectToIdentityProviderForSignOut .

Tratcher commented 2 years ago

Owin/Katana has a shared RedirectToIdentityProvider and you need to check the message type: https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.protocols.openidconnect.openidconnectmessage.requesttype?view=azure-dotnet#Microsoft_IdentityModel_Protocols_OpenIdConnect_OpenIdConnectMessage_RequestType

HttpContext.GetOwinContext().Authentication.SignOut(new AuthenticationProperties { RedirectUri = ConfigurationManager.AppSettings["OpenIdRedirectPostLogout"] }, "oidc");

You didn't name your auth provider "oidc", it's using the default name OpenIdConnectAuthenticationDefaults.AuthenticationType "OpenIdConnect".

Schoof-T commented 2 years ago

Owin/Katana has a shared RedirectToIdentityProvider and you need to check the message type: https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.protocols.openidconnect.openidconnectmessage.requesttype?view=azure-dotnet#Microsoft_IdentityModel_Protocols_OpenIdConnect_OpenIdConnectMessage_RequestType

HttpContext.GetOwinContext().Authentication.SignOut(new AuthenticationProperties { RedirectUri = ConfigurationManager.AppSettings["OpenIdRedirectPostLogout"] }, "oidc");

You didn't name your auth provider "oidc", it's using the default name OpenIdConnectAuthenticationDefaults.AuthenticationType "OpenIdConnect".

Thank you! I'm almost there I think, I get redirected now, the token is filled in but I get the error "Not an RSA algorithm." from our identityprovider.

RedirectToIdentityProvider = n =>
                    {
                        if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                        {
                            var result = n.OwinContext.Authentication.AuthenticateAsync("Cookies").Result;
                            string token = result.Properties.Dictionary["access_token"];

                            if (!string.IsNullOrEmpty(token))
                            {
                                n.ProtocolMessage.IdTokenHint = token;
                            }
                        }
                        return Task.FromResult(0);
                    }
Tratcher commented 2 years ago

Was that the right token?

Schoof-T commented 2 years ago

Was that the right token?

It was not, my apologies! And thank you for all the help. :)