aspnet / AspNetKatana

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

Share oauth state between Owin app instances #448

Closed LeaFrock closed 2 years ago

LeaFrock commented 2 years ago

Recently we're upgrading the auth way of a webform app to OpenIdConnect. We've imported Microsoft.Owin.Security.OpenIdConnect and it works well if the app is deployed as singleton. But when we deploy more instances and add load balance, the apps throw exceptions related to require nonce or require state.

I find that it seems to be the problem of default DataProtector Owin uses. If a challenge is requested by app-1, and the user logins successfully in Identity Server, then the server redirects the browser to xxx/signin-oidc?code=xxx&state=xxx&.... Due to load balance, the callback is sent to app-2, and app-2 cannot unprotect state and finally stops the next step.

Would anyone like to give me a solution, please?

p.s. I find this issue #435, but in fact I'm not going to share something with a .NET Core app. It will import more Nuget packages which are not updated after 2018. I just want to make sure the instances of same app can protect/unprotect each other.

Tratcher commented 2 years ago

Yes, this is a DataProtection configuration issue. Are you running this in IIS or on HttpListener? In IIS you only need to set up a shared machine key. Outside of IIS the DataProtection configuration is a bit more manual.

LeaFrock commented 2 years ago

Thanks for you reply! @Tratcher

It does not work. image

Maybe there're more problems. For example, I notice that, RememberNonce will store nonce cookie into memory of app-1, and when RetrieveNonce reads nonce from CookieManager of app-2, will it always return null and then make the validation fail?

On our test environment the webform app is deployed as singleton and everything runs well. Now it becomes a disaster after we push the new version to online, and we have to roll back and solve the problems. I'm still not sure about the reason and looking for help about locating the point.

Tratcher commented 2 years ago

Maybe there're more problems. For example, I notice that, RememberNonce will store nonce cookie into memory of app-1, and when RetrieveNonce reads nonce from CookieManager of app-2, will it always return null and then make the validation fail?

Cookies are only stored on the client, not on the server.

Are you able to set up a repro and attach a debugger? The symbols should be available.

LeaFrock commented 2 years ago

Yep. After two days working on this issue, I find that it's the problem of load balance system from our cloud service provider.

The nonce cookie is set by CookieManager.AppendResponseCookie. Without using LB, I can see Set-Cookie: OpenIdConnect.nonce.xxxxx in the http response headers; but as long as LB is used, the Set-Cookie is gone. Because the problem of LB can't be solved immediately, finally I have to set RequireNonce to false.

However, I find another interesting question. If RequireNonce is false, we get another exception: IX21329: RequireState xxxx.

At the very beginning, we make a Nuget package for our own productions for easier usage,


    public class MyAuthenticationOptions : OpenIdConnectAuthenticationOptions
    {
        public MyAuthenticationOptions() : base(OpenIdConnectAuthenticationDefaults.AuthenticationType)
        {
            SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType;
            ResponseType = OpenIdConnectResponseType.Code;
            RedeemCode = true;
            Scope = "xxx";
            ResponseMode = OpenIdConnectResponseMode.Query;
            SaveTokens = true;
            BackchannelTimeout = TimeSpan.FromSeconds(5.0);
            PostLogoutRedirectUri = "/";
            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                SecurityTokenValidated = GetUserInformationAsync,
                RedirectToIdentityProvider = SetIdTokenHint
            };
        }
   }

with an extension method,


        public static IAppBuilder UseMyAuthentication(this IAppBuilder app, Action<MyAuthenticationOptions> configuration)
        {
            var options = new MyAuthenticationOptions()
            {
                Authority = "mydomain",
            };
            configuration?.Invoke(options);

            // other codes

            app.UseOpenIdConnectAuthentication(options);

            return app;
        }

I notice that the base constructor has already initialized ProtocolValidator, and the ValidateState in OpenIdConnectProtocolValidator should always be skipped when RequireStateValidation is false.

            ProtocolValidator = new OpenIdConnectProtocolValidator()
            {
                RequireStateValidation = false,
                NonceLifetime = TimeSpan.FromMinutes(15)
            };

But now, I have to copy these codes to MyAuthenticationOptions, or to UseMyAuthentication, and then the RequireState exceptions are gone.


app.UseMyAuthentication(config => 
{
    // other codes
   config.ProtocolValidator = new OpenIdConnectProtocolValidator()
            {
                RequireStateValidation = false,
                NonceLifetime = TimeSpan.FromMinutes(15)
            };
})

I'm still not sure why the ProtocolValidator is not intialized as expected without extra codes above. But now it works well online, and I can take a good sleep at least 😞 .

LeaFrock commented 2 years ago

The nonce cookie is set by CookieManager.AppendResponseCookie. Without using LB, I can see Set-Cookie: OpenIdConnect.nonce.xxxxx in the http response headers; but as long as LB is used, the Set-Cookie is gone.

New progress. The nonce cookie is set by the codes,

Options.CookieManager.AppendResponseCookie(
                Context,
                GetNonceKey(nonce),
                Convert.ToBase64String(Encoding.UTF8.GetBytes(Options.StateDataFormat.Protect(properties))),
                new CookieOptions
                {
                    SameSite = SameSiteMode.None,
                    HttpOnly = true,
                    Secure = Request.IsSecure,
                    Expires = DateTime.UtcNow + Options.ProtocolValidator.NonceLifetime
                });

Notice the line, Secure = Request.IsSecure. Our load balance system will hold all https related works, and then just forward them to the app instances in http. Therefore, the IsSecure is false, and finally set a cookie like below,

image

A valid cookie with SameSite=None must have Secure too, otherwise the browser will abandon it. So it seems to be the real problem. If so, is there any solution or workground?

p.s. Why not just set Secure=true as same as HttpOnly?

p.s.2 I also check the codes of ASP.NET Core here, it has CookieBuilder for nonce which is a pretty solution for my problem. But for Owin, there is not the same property 😢 .

Tratcher commented 2 years ago

The recomendation is to update the request fields to match the public endpoint (scheme, host, port) so that when link and cookies are generated they use the correct value. See similar asp.net core samples: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-6.0#scenarios-and-use-cases

LeaFrock commented 2 years ago

Thanks for your answer. And also sorry that, I just find the similar issue #332, #352 about nonce cookie problem 😿 .