DuendeSoftware / Support

Support for Duende Software products
20 stars 0 forks source link

Inactivity Timeout Setup #1267

Closed gryffs closed 2 months ago

gryffs commented 3 months ago

Which version of Duende IdentityServer are you using? 7.0.4

Which version of .NET are you using? 8.0.4

Describe the bug

We are using IdentityServer with settings in place to follow the Inactivity Timeout method described here: https://docs.duendesoftware.com/identityserver/v7/ui/server_side_sessions/inactivity_timeout/ Our IDP is accessed by several clients, some are setup to use Refresh Token Rotation for a SPAs, we have offline access for mobile, and then we have SPAs that we have moved to using BFF and backchannel logout. I say all of this to give context to how our IDP is used, but my main focus with this Issue is around Inactivity Timeout and our BFF SPAs.

Within our environment, our intent has been to terminate any sessions that have been inactive for more than 15 minutes. But, we seem to be experiencing issues with random 401s that do not correlate with an actual expired session. It has proven very hard to reproduce. Within our system, we monitor for user activity (some areas the user may be interacting with data that is not causing network traffic) and if the session is past the halfway point of expiration and they are active, we send a request to an endpoint on our IDP that is appropriately routed through the SPA Backend (Duende BFF) with the desire to cause the session to be extended. From what we can tell, the 401s that have been observed are coming from this endpoint and the session is still active.

Along with this issue, we have reports of inconsistent session lifetime and random logouts from users. This has even been experienced by folks on our dev team, but they have proven very hard to recreate in any manner that we can further troubleshoot.

I suspect that the issue comes down to our setup and the time limits we have in place. I will give our setup information below in the additional context section.

To Reproduce

We are unable to provide a way to consistently reproduce the issue. I apologize for the lack of reproducibility and so I hope to get additional eyes on our setup and configuration.

Expected behavior

Expected behavior is for our BFF related SPAs, through back channel logout, server side sessions, and Session Coordination, to implement a successful inactivity timeout of approximately 15 minutes along with a consistent user session experience.

Log output/exception with stacktrace

Nothing included at this time.

Additional context

Duende IdentityServer Setup:

builder.Services.AddIdentityServer(options =>
    {
        options.LicenseKey = config.GetSection("Duende").GetValue<string>("LicenseKey");
        options.KeyManagement.Enabled = false;
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;
        options.UserInteraction.LoginUrl = "/Account/Login";
        options.UserInteraction.LogoutUrl = "/Account/Logout";
        options.Authentication = new Duende.IdentityServer.Configuration.AuthenticationOptions()
        {
            CookieLifetime = TimeSpan.FromMinutes(15), // ID server cookie timeout set to 15 Minutes
            CookieSlidingExpiration = true,
            CoordinateClientLifetimesWithUserSession = true
        };
        options.ServerSideSessions.UserDisplayNameClaimType = "name";
        options.ServerSideSessions.RemoveExpiredSessions = true;
        options.ServerSideSessions.RemoveExpiredSessionsBatchSize = 100;
        options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;
        options.ServerSideSessions.RemoveExpiredSessionsFrequency = TimeSpan.FromMinutes(1);
    })
    .AddSigningCredential(x509Certificate2.ActiveCertificate)
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
    })
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
        options.EnableTokenCleanup = true;
    })
    .AddAspNetIdentity<ApplicationUser>()
    .AddServerSideSessions();

builder.Services.ConfigureApplicationCookie(options =>
{
    options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
});

BFF Setup:

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
    options.DefaultSignOutScheme = "oidc";
})
    .AddCookie("Cookies", options =>
    {
        options.Cookie.Name = "__[removed]-bff";
        options.Cookie.SameSite = SameSiteMode.Strict;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(configuration.GetValue<int>("CookieExpire")); // set to 15 Minutes
        options.SlidingExpiration = true;
    })

Client Settings: Require PKCE: True Identity Token Lifetime: 300s Access Token Lifetime: 3600s Access Token Type: JWT Always Include User Claims in ID Token: True Authorization Code Lifetime: 300s Allow Offline Access: True Refresh Token Expiration: Absolute Absolute Refresh Token Lifetime: 43200s Refresh Token Usage: Reuse Back Channel Logout Session Required: True Back Channel Logout URI: https://oururl.com/bff/backchannel

Thank you in advance for any help and guidance.

AndersAbel commented 2 months ago

We send a request to an endpoint on our IDP that is appropriately routed through the SPA Backend (Duende BFF) with the desire to cause the session to be extended.

Can you please explain this a bit more in depth? Do you call an API on the Idp through the BFF? In that case it will not cause any sessions to be extended by the API call itself. The API call in itself won't extend any sessions at all. However if the API causes the access token to be refreshed, that would trigger a session extensions thanks to the refresh token flow being invoked.

This actually shows an inconsistency in your configuration that could explain the issue. The access token lifetime is 3600 seconds, which is far longer than the session lifetime. The existing access token might be used without any reason to run a refresh token flow, even though the session is about to expire. I would recommend decreasing the access token lifetime to ensure that it is always lower than the session lifetime.

gryffs commented 2 months ago

Thank you for the response. I apologize for not giving that part of the context, but yes, we have several APIs that we interact with within our infrastructure. The endpoint I mentioned, that exists on our IDP, is in the form of an API call. For example:

    [Authorize(LocalApi.PolicyName)]
    [HttpGet]
    [Route("/api/account/activity")]
    public IActionResult AccountActivity()
    {
        return NoContent();
    }

This specific API call was created with the desire to invoke the refresh token flow when a user is actively using our application but not creating any API traffic that would invoke the refresh token flow otherwise. Also, this is just one case that we have been able to narrow some of the inconsistencies down to but we accept the fact that there are other inconsistencies.
With that said, is there a recommended time ratio between the access token lifetime and the session lifetime given the desire for activity to slide the session time?

AndersAbel commented 2 months ago

I think that you are fairly close to a working solution:

  1. The access token lifetime must be shorter than the session lifetime, let's say 10 minutes.
  2. Your call to the AccountActivity() endpoint must happen in the time window between the access token expiry and the session expiry, let's say after 8 minutes.

You do not even need to call an API for the access token to be renewed. You can also call GetUserAccessTokenAsync() and discard the returned token.

gryffs commented 2 months ago

Thank you again for your help. I’m still having a hard time understanding the interaction between the access token length and the session and how this could cause the issue.
So, if I understand the example above, if the access token lifetime is set to 10 minutes and the session lifetime is set to 15 minutes (sliding) and there is activity at 8 minutes, then the session will be extended (because it was past the half life mark). But, if the access token is still good (it expires in 10 minutes and we had the activity at 8 minutes) then that access token will remain in use for another two minutes. After 10 minutes are up, that access token is no longer valid but the session is still valid and has the Absolute Refresh token to request another access token. So, our system in place for using Duende BFF and Inactivity Timeout Setup will handle requesting that new access token if it is needed during the life of the session. Does that mean that there can be a window of time where a valid session is without a valid access token because it will simply request a new access token when needed? If there is a valid access token that lasts longer than the session, how does this cause an authorization issue if there is still a valid refresh token that can be used to request another access token (I’m not asking this question in an attempt to validate a long lived token, but to understand the underlying cause of our issues, I’ve already dropped our access token lifetime to 10 minutes, but I don’t fully understand why yet)?

AndersAbel commented 2 months ago

If the access token lifetime is set to 10 minutes and the session lifetime is set to 15 minutes (sliding) and there is activity at 8 minutes, then the session will be extended (because it was past the half life mark).

The session on the host where there was activity will be extended. If you have 15 minutes session timeout on both your BFF and IdentityServer host this will extend the BFF session length. The IdentityServer session will not be affected as there is no interaction with the IdentityServer.

But, if the access token is still good (it expires in 10 minutes and we had the activity at 8 minutes) then that access token will remain in use for another two minutes.

Yes

After 10 minutes are up, that access token is no longer valid but the session is still valid and has the Absolute Refresh token to request another access token. So, our system in place for using Duende BFF and Inactivity Timeout Setup will handle requesting that new access token if it is needed during the life of the session.

Yes.

Does that mean that there can be a window of time where a valid session is without a valid access token because it will simply request a new access token when needed?

Yes.

If there is a valid access token that lasts longer than the session, how does this cause an authorization issue if there is still a valid refresh token that can be used to request another access token (I’m not asking this question in an attempt to validate a long lived token, but to understand the underlying cause of our issues, I’ve already dropped our access token lifetime to 10 minutes, but I don’t fully understand why yet)?

If you have server side sessions and CoordinateClientLifetimesWithUserSession = true then this case is not possible. When the session on IdentityServer expires it will also revoke the refresh token.

If you have CoordinateClientLifetimesWithUserSession = false then this is indeed possible and then the refresh token would be used to get new refresh token even if the session on IdentityServer is expired.

gryffs commented 2 months ago

Thank you for your help and clarification. I believe this puts us in a good place with not only correcting the issue but with a broader understanding.