aspnet / AspNetKatana

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

OpenIdConnect terminates the session in 5 minutes #489

Closed tomburger closed 1 year ago

tomburger commented 1 year ago

We have ASP.NET app hosted in Azure, using CookieAuthentication. When user logs in, session stays valid (possibly hours or days, we haven't measured exactly).

When we add OpenIdConnectAuthentication, the session gets terminated after 5 minutes of inactivity.

It is not necessary to use that OpenIdConnect to login. Even if user has used cookie based login, after 5 minutes of inactivity, the next request gets redirected to RedirectToIdentityProvider notification handler of OpenIdConnect, but at that time, the user session is already gone.

ASP.NET is using version 4.8, Owin libraries have version 4.2.2.

Is there any settings, which would make that session to last longer? Or settings, where OpenIdConnect would not interfere at all with sessions, it does not own?

tomburger commented 1 year ago

This is how our configuration is setup:

public void ConfigureAuth(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/login")
    });

    var authority = "https://login.microsoftonline.com/common/v2.0";
    var redirectUri = "https://app.<our-domain>.net/.auth/login/aad/callback";
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ApplicationCookie);
    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
        ClientId = clientId,
        Authority = authority,
        Scope = OpenIdConnectScope.OpenIdProfile,
        ResponseType = OpenIdConnectResponseType.CodeIdToken,
        RedirectUri = redirectUri,
        PostLogoutRedirectUri = "http://www.<our-domain>.net",
        TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
        },
        Notifications = new OpenIdConnectAuthenticationNotifications()
        {
            SecurityTokenValidated = (context) =>
            {
                return Task.FromResult(0);
            },
            RedirectToIdentityProvider = (context) =>
            {
                context.OwinContext.Response.Redirect("/login?sso-logged-me-off");
                context.HandleResponse(); // Suppress the exception
                return Task.FromResult(0);
            },
            AuthenticationFailed = (context) =>
            {
                // Pass in the context back to the app
                var msg = context.Exception?.Message ?? "Unknown error";
                context.OwinContext.Response.Redirect("/Account/SsoError?msg=" + HttpUtility.UrlEncode(msg));
                context.HandleResponse(); // Suppress the exception
                return Task.FromResult(0);
            }
        }
    });
}
Tratcher commented 1 year ago

OIDC can't interfere with sessions it does not own, it only runs when invoked.

Start with UseTokenLifetime = false to see if that helps: https://github.com/aspnet/AspNetKatana/blob/423f80e7f82708e3f78a596cf405a2a5595c12f1/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs#L298-L302

tomburger commented 1 year ago

@Tratcher thanks for the hint...

OIDC can't interfere with sessions it does not own, it only runs when invoked.

I know, it should not, but nevertheless it does. If I have the line app.UseOpenIdConnectAuthentication in Startup.Auth.cs of my ASP.NET app, the session expires in 5 minutes, if I remove that single line, the session does not expire.

Start with UseTokenLifetime = false to see if that helps

Ok, I tried and it does not help. Any other idea?

Tratcher commented 1 year ago

How are you authenticating users and creating sessions when you don't have UseOpenIdConnectAuthentication?

tomburger commented 1 year ago

We call UseCookieAuthentication in Startup.Auth.cs, see above, and when form with user and password is submitted, we call HttpContext.GetOwinContext().Authentication.SignIn(). For login with OpenId, we do not call SignIn, but Challenge with redirect URL and we call SignIn only after redirect comes back with the claim.

Tratcher commented 1 year ago

What are you passing to SignIn?

tomburger commented 1 year ago

The code looks like this:

var identity = await App.Users.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
App.AuthManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, identity);

App.Users is reference to Microsoft.AspNet.Identity.UserManager<T> and App.AuthManager is reference to HttpContext.GetOwinContext().Authentication in controller, user is model coming from binding of action, and it contains username and password.

Tratcher commented 1 year ago

Can out put a breakpoint in each OpenIdConnectAuthenticationNotifications event and see if any of them trigger on requests where you're doing local signin, or on the request that fails after some time?

Also, I see the cookie might be marked as IsPersistent? How long of an expiration does the client think it has?

tomburger commented 1 year ago

The only notification called is RedirectToIdentityProvider and when it is called, session is gone already (controller.User.Identity.IsAuthenticated is false).

Here is the list of values in ProtocolMessage (I have removed properties with null):

{
    "ClientId": "<our-client-id>",
    "EnableTelemetryParameters": true,
    "Nonce": "6380971290729...ZTY2LTg4OTQtMzAzNjY1YzAzNmE1",
    "RedirectUri": "http://localhost:44301/.auth/login/aad/callback",
    "RequestType": 0,
    "ResponseMode": "form_post",
    "ResponseType": "code id_token",
    "Scope": "openid profile",
    "SkuTelemetryValue": "ID_NET461",
    "State": "OpenIdConnect.AuthenticationProperties=ktOYNghqNij4qdEiEdoj....XWVq-M0qHH8-bF2fno2Z7DQ",
    "IssuerAddress": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
    "Parameters": {
        "client_id": "<our-client-id>",
        "redirect_uri": "http://localhost:44301/.auth/login/aad/callback",
        "response_type": "code id_token",
        "scope": "openid profile",
        "state": "OpenIdConnect.AuthenticationProperties=ktOYNghqNij4qdEiEdoj....XWVq-M0qHH8-bF2fno2Z7DQ",
        "response_mode": "form_post",
        "nonce": "63809712907296...zNjY1YzAzNmE1"
    },
    "PostTitle": "Working...",
    "Script": "<script language=\"javascript\">window.setTimeout('document.forms[0].submit()', 0);</script>",
    "ScriptButtonText": "Submit",
    "ScriptDisabledText": "Script is disabled. Click Submit to continue."
}

From that I assume it is this call: https://github.com/aspnet/AspNetKatana/blob/dbe159e43e2eee44f315f26268943e8ab5a4f60d/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L103 Unfortunately stack trace only says RedirectToIdentityProvider and then [External code].

Tratcher commented 1 year ago

Unfortunately stack trace only says RedirectToIdentityProvider and then [External code].

There's an option to right-click on the stack trace and check "Show External Code".

That message looks like it's coming from Challenge, not SignOut. https://github.com/aspnet/AspNetKatana/blob/dbe159e43e2eee44f315f26268943e8ab5a4f60d/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs#L203

It's interesting that you're automatically challenging for remote auth. I'd have expected you to challenge for local auth.

The other thing to check is Fiddler, making sure the app cookie is included on all these requests.

tomburger commented 1 year ago

You are right, it is coming from ApplyResponseChallengeAsync. Here is a complete call stack (thanks for the hint):

image

...and yes, that request, which ends up in the RedirectToIdentityProvider and continues then with redirect login page, that request receives cookie called .AspNet.ApplicationCookie.

Tratcher commented 1 year ago

So which controller did that request go to, and why did that controller challenge for OIDC instead of app cookies?

tomburger commented 1 year ago

So which controller did that request go to...

Just ordinary application controller, but it actually never reaches it. Controller is protected by [Authorize] attribute, which diverts the flow into OIDC challenge, before reaching the controller code.

and why did that controller challenge for OIDC instead of app cookies?

...that's the whole point I guess, to find out why it is happening. ;-)

One observation I have made today is the following: the problem is not checking the cookie after that 5 minutes interval, but it must be somehow already encoded in that cookie, when it is generated. Why? When I comment out OIDC, run the server and login, then bring OIDC back in and restart the server, my session is still valid and it lasts longer than 5 minutes without problem. Only when I logout and login, the cookie, I get, lasts only 5 minutes.

Does that help?

Tratcher commented 1 year ago

Controller is protected by [Authorize] attribute, which diverts the flow into OIDC challenge, before reaching the controller code.

Ah. OIDC defaults to Active mode where it intercepts any 401 response (such as that from the Authorize attribute). You can turn that off. https://github.com/aspnet/AspNetKatana/blob/423f80e7f82708e3f78a596cf405a2a5595c12f1/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs#L57

That doesn't explain why you're getting a 401 though. That would be an issue with the cookie itself, not the OIDC handler. Without the OIDC handler the 401 would be handled by CookieAuth. If that flow is automatic then you might be silently signed in without realizing it. You can hook up to the cookie auth provider events to be sure.

tomburger commented 1 year ago

Turning authentication mode to passive helped with OIDC, intercepting 401. The session is still valid only 5 minutes, but now the redirect to login screen is not generated from OIDC.

But still, the initial problem stays: if we call UseOpenIdConnectAuthentication, then authentication cookie is valid only 5 minutes, even if we logged in using username and password. If we do not call it, the authentication cookie holds longer. Can we somehow get this resolved? At best, getting UseOpenIdConnectAuthentication to allow cookie valid for more than 5 minutes (at the end, even for people using AAD to login, we want session to last longer).

Also, should we still keep UseTokenLifetime as false? Remember? That's what we started with.

Tratcher commented 1 year ago

At least something has worked. I still do not understand what would cause the cookie lifetime issue, OIDC isn't involved in reading or writing the cookie. The lifetime would have to be specified in the AuthenticationProperties when signing in and creating the cookie. UseTokenLifetime = true would explain this if the OIDC tokens were only valid for 5 min, but only when signing in with OIDC.

Hooking Cookie's OnResponseSignin will allow you to inspect the AuthenticationProperties and figure out if there is a timeout specified (and override it). The next trick would be figuring out who set it. https://github.com/aspnet/AspNetKatana/blob/423f80e7f82708e3f78a596cf405a2a5595c12f1/src/Microsoft.Owin.Security.Cookies/Provider/CookieAuthenticationProvider.cs#L22

tomburger commented 1 year ago

Sorry for the late response, here is current status:

In general, problem is not solved, but we have found an arrangement, where problem is not happening.

@Tratcher I leave it up to you - either we can close this issue as resolved, or if you want to investigate further, let me know, I can isolate the problem from our application codebase into standalone GitHub repo and we can investigate healthy coexistence of both authentication schemas until we find out what's wrong.

Tratcher commented 1 year ago

An isolated repro would help. You'd rather not maintain separate instances, right?

tomburger commented 1 year ago

Hi @Tratcher ,

sorry for the delay, but I have now isolated repro for you on following repository: https://github.com/tomburger/aspnet-auth-sample

Just clone it, open solution in src folder and run it in Visual Studio. It has a login screen and there are four users available, ringo@beat.les, john@.., paul@.. and george@.., password "LetItBe" for all four of them. After login there are two pages and you can navigate between them with button. If you wait for 5 minutes and then you click the button, you will be logged out and redirected back to login screen.

If you go to file src\App_Start\Startup.Auth.cs and comment out OIDC part (lines 25-79), then logout after 5 minutes is not happening.

Please, notice that you are still using cookie authentication, the difference is only the call to UseOpenIdConnectAuthentication. If you call it, your session is terminated after 5 minutes, if you do not call it, it will stay valid much longer.

Feel free to ask more questions, or send me the pull request, if you know how to fix it. We can keep the repo then as a reference for generations to come ;-)

Tratcher commented 1 year ago

This took me way longer to figure out than it should have 😆.

The culprit is app.UseExternalSignInCookie(DefaultAuthenticationTypes.ApplicationCookie); https://github.com/aspnet/AspNetIdentity/blob/b7826741279450c58b230ece98bd04b4815beabf/src/Microsoft.AspNet.Identity.Owin/Extensions/AppBuilderExtensions.cs#L119-L124

UseExternalSignInCookie doesn't just set a field, it creates another Cookie Auth instance with a 5min timeout. You'd passed the same name to that one as the other cookie auth instance so it was ambiguous.

The API you want instead is SetDefaultSignInAsAuthenticationType

tomburger commented 1 year ago

Sorry, @Tratcher, for late reply. I have fixed the sample and I can confirm it works. Thanks a lot for your help.