Search code examples
asp.netauthorizationidentityserver4

Refresh IdentityServer cookie when client cookie slides


We have multiple apps that our users may login to via our IdentityServer. They are all using cookie auth to secure the sites, with tokens to secure their related APIs. Due to the functionality of some of the sites, the cookies need to slide which is working.

The problem that is causing is that when the cookie slides, it does not contact our IdentityServer, which means the cookie on the server may time out. This is fine while the user stays only in the apps they were already in, but once they connect to another app, it hits the IdentityServer (whose cookie has timed out) and is prompted to login again despite being active in the other apps.

So what I'd like to have happen is whenever the client cookie slides, it still hits the IdentityServer to cause its cookie to slide with it. I'm open to other suggestions, but I haven't had any other ideas myself and since our applications span a variety of .net platforms (WebForms, MVC, Core) and toolsets (Telerik in particular) I'm trying to come up with a generic solution like this.

Code:

IdentityServer cookie

        // Configure auth cookie
        services.AddAuthentication(cookieSettings.CookieName)
                .AddCookie(cookieSettings.CookieName, 
                           options =>
                           {
                               options.ExpireTimeSpan = TimeSpan.FromMinutes(cookieSettings.Expiration); // 4 hours
                               options.SlidingExpiration = cookieSettings.AllowSliding; //true
                           });

Client

public static void ConfigureOAuthForWebForms(this IAppBuilder app, OAuthSettings configuration)
{
    // Use Cookies to Store JWT Token for Web Browsers
    var cookieAuthenticationOptions = new CookieAuthenticationOptions
    {
        AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
        CookieDomain = configuration.CookieDomain,
        CookieName = configuration.CookieName,
        CookieHttpOnly = configuration.CookieHttpOnly,
        ExpireTimeSpan = configuration.AuthTimeout,
        SlidingExpiration = configuration.AllowSlidingAuthTimeout
    };

    app.UseCookieAuthentication(cookieAuthenticationOptions);

    // Turn off the JWT claim type mapping to allow well-known claims (e.g. ‘sub’ and ‘idp’) to flow through
    JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();

    var openIdConnectAuthenticationOptions = new OpenIdConnectAuthenticationOptions
    {
        AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
        Authority = configuration.AuthorizationServerUri,
        ClientId = configuration.Client,
        PostLogoutRedirectUri = configuration.RedirectUri,
        RedirectUri = configuration.RedirectUri,
        ResponseType = configuration.ResponseType,
        Scope = configuration.Scope,
        SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
        UseTokenLifetime = configuration.UseAuthServerLifetime, // false
        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            SecurityTokenValidated = async n =>
            {
                var claimsToExclude = new[] { "aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash", "c_hash", "idp", "amr" };

                var claimsToKeep = n.AuthenticationTicket.Identity.Claims.Where(x => false == claimsToExclude.Contains(x.Type)).ToList();
                claimsToKeep.Add(new Claim(AuthConstants.AuthenticationToken, n.ProtocolMessage.IdToken));

                if (n.ProtocolMessage.AccessToken != null)
                {
                    // Add access_token so we don't need to request it when calling APIs
                    claimsToKeep.Add(new Claim(AuthConstants.AccessToken, n.ProtocolMessage.AccessToken));

                    var userInfoClient = new UserInfoClient(new Uri(n.Options.Authority + "connect/userinfo").ToString());
                    var userInfoResponse = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);
                    var userInfoClaims = userInfoResponse.Claims
                        .Where(x => x.Type != "sub") // filter sub since we're already getting it from id_token
                        .Select(x => new Claim(x.Type, x.Value));
                    claimsToKeep.AddRange(userInfoClaims);
                }

                var ci = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
                ci.AddClaims(claimsToKeep);

                if (!configuration.UseAuthServerLifetime)
                {
                    n.AuthenticationTicket.Properties.IsPersistent = true;
                    n.AuthenticationTicket.Properties.ExpiresUtc = DateTimeOffset.UtcNow.Add(configuration.AuthTimeout); // 2 hours
                    n.AuthenticationTicket.Properties.AllowRefresh = configuration.AllowSlidingAuthTimeout; // true
                }

                n.AuthenticationTicket = new AuthenticationTicket(ci, n.AuthenticationTicket.Properties);
            },
            RedirectToIdentityProvider = n =>
            {
                if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                    n.ProtocolMessage.IdTokenHint = n.OwinContext.Authentication.User.FindFirst(AuthConstants.AuthenticationToken)?.Value;

                return Task.FromResult(0);
            }
        }
    };

    // Authenticate to Auth Server
    app.UseOpenIdConnectAuthentication(openIdConnectAuthenticationOptions);

    app.UseStageMarker(PipelineStage.Authenticate);
}

Solution

  • I’d just make the session length on the IDP much longer and then there’s no need to slide it. You can control whether the user should authenticate again from any given client should you wish using the max_age parameter.