Home Sliding Session on Web API Request
Reply: 5

Sliding Session on Web API Request

Mike Perrenoud
1#
Mike Perrenoud Published in 2018-01-10 14:42:55Z

UPDATE: it appears it is trying to write the new cookie header in ApplyResponseGrantAsync but can't because it's throwing an exception that the headers are already sent.

UPDATE: to be clearer. How do I get the Set-Cookie header added to the XHR response during a Web API request?

TL;DR; The issue is that the application is authenticated with MVC, but makes heavy use of Web API. The Web API requests do not slide the session even though they use the Authentication attribute - almost certainly because it's a cookie response.

I have a combined MVC and Web API application. For the most part, the MVC views just load SPA's that make lots of Web API requests. This is fine except the session sliding does not work with Web API requests.

I can see where the CookieAuthenticationHandler slides the session in ApplyResponseGrantAsync, but I need to force that on every Web API request.

model.Properties.IssuedUtc = new DateTimeOffset?(this._renewIssuedUtc);
model.Properties.ExpiresUtc = new DateTimeOffset?(this._renewExpiresUtc);
if (this.Options.SessionStore != null && this._sessionKey != null)
{
  await this.Options.SessionStore.RenewAsync(this._sessionKey, model);
  ClaimsIdentity identity = new ClaimsIdentity((IEnumerable<Claim>) new Claim[1]
  {
    new Claim("Microsoft.Owin.Security.Cookies-SessionId", this._sessionKey)
  }, this.Options.AuthenticationType);
    model = new AuthenticationTicket(identity, (AuthenticationProperties) null);
}
string cookieValue = this.Options.TicketDataFormat.Protect(model);
if (model.Properties.IsPersistent)
  cookieOptions.Expires = new DateTime?(this._renewExpiresUtc.ToUniversalTime().DateTime);
this.Options.CookieManager.AppendResponseCookie(this.Context, this.Options.CookieName, cookieValue, cookieOptions);

Does anybody know how to force this?

Authentication Setup

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(15),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    },
    SlidingExpiration = true,
    ExpireTimeSpan = TimeSpan.FromMinutes(1)
});

app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
Arvin
2#
Arvin Reply to 2018-01-18 09:49:17Z

According to CookieAuthenticationMiddleware source, if you set SlidingExpiration for CookieAuthenticationOptions to true, your session would be renew on every request. You can check this code to see yourself.

I think I track down your problem, based on this line in order for cookie to renew, the AllowRefresh property of AuthenticationProperties of SignIn method should be true. I'm guessing this is your problem.

If I'm not wrong you are using `ApplicationSignInManager.SignInAsync()` in order to sign in your users. Unfortunately based on the [implementation][2] there's no way to pass a parameter to set `AllowRefresh`, So I'm suggesting to override SignInAsync method and set `AllowRefresh` to true, and see if it works.

UPDATE

This is another guess, it may solve your problem. Based on this line a cookie session would renew when only 50% of its lifetime has passed and as I looked at the code I saw no way to override this. I suggest a workaround for renewing cookie by adding a Middleware which would SingIn user on each request, hence creating a new cookie on each request, and it doesn't matter if the request if in WebAPI or MVC.

Add the middleware after app.UseCookieAuthentication

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
        validateInterval: TimeSpan.FromMinutes(15),
        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
},
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(1)
});

app.Use(async (context, next) =>
{
    var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
    if (result != null)
    {
        result.Properties.ExpiresUtc = null;
        result.Properties.IssuedUtc = null;
        context.Authentication.SignIn(result.Properties, result.Identity);
    }
    await next();
});

basically you can set SlidingExpiration to false, because it don't matter with this workaround and also in this way OnValidateIdentity on CookieAuthentication could not reload user Claims every 30 minutes, because IssuedUTC is being refreshed on each request, if you need this you have to modify this middleware and write a custom OnValidateIdentity

norcal johnny
3#
norcal johnny Reply to 2018-01-13 06:05:30Z

Generally, it'll work if you enable slidingExpiration in web.config. You have no need to regenerate a new cookie manually. For your scenario, I suggest you to use a trace tool e.g. Fiddler. When you refresh the page, you can check from Fiddler that whether the cookie expired time is reset.

Amirhossein Mehrvarzi
4#
Amirhossein Mehrvarzi Reply to 2018-01-15 19:27:17Z

The ApplyResponseAsync(), and subsequently ApplyResponseGrantAsync() and ApplyResponseChallengeAsync(), will also be called once when the headers are being written to the client due to the AuthenticationHandler hooking up a callback using Response.OnSendingHeaders() during the initialization phase. This makes sure that all handlers have a chance to create a challenge, or handle a response grant, right before the headers are sent to the user, which is the last point in time that we can modify the response.

So Why you need to build a cookie response in your case?! Why you don't use a suitable timeout interval (SlidingExpiration)? Or at least use a persistent cookie.

Edit

Some people who have trouble like yours solved by enabling slidingExpiration in web.config Upon adding the cookieless="UseCookies" like the following. Please let me know the result:

<forms loginUrl="/account/login" name="authy" path="/" slidingExpiration="true" cookieless="UseCookies"/>
Majid ALSarra
5#
Majid ALSarra Reply to 2018-01-18 11:20:37Z

TL;DR

Try this: Just add this code to your controller, it is overriding OnActionExecuted which runs after every actions and adds the header manually:

       protected override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            if (filterContext.ActionDescriptor.ActionName=="YourWebAPIName")
            {
                filterContext.HttpContext.Response.AddHeader("Set-Cookie","CookieName=CookieValue");           
            }
        //You can replace the former if with a more general one like checking:
        //filterContext.Result.GetType() and see if it is of the type JsonResult

            base.OnActionExecuted(filterContext);
        }

------------------Following is my old answer-----------------------------

First consider @Amirhossein Mehrvarzi questions, if for whatever reasons you need to create your own session sliding consider the following:

This is just a suggestion, not sure if it will work

You may try to override either:

OnActionExecuting(ActionExecutingContext filterContext) (runs before action)

or Controller.OnActionExecuted(ActionExecutedContext filterContext) (runs after action)

operations of the control and slide the session there.

It'll be executed on both WebApi controllersand page controllers, you can know which controller using the filterContext.ActionDescriptor.ActionName as in this link

both methods will receive useful inputs that can manipulate the action execution as in the following links:

ActionExecutedContext

ActionExecutingContext

Mike Perrenoud
6#
Mike Perrenoud Reply to 2018-01-18 14:05:18Z

It ended up being a filter I had added to the pipeline to log the content of the response. After removing that filter the cookie was being written, even by Web API requests.

public class WebApiResponseFilter : ActionFilterAttribute
{
    private ILogUtils logUtils;

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);

        if (logUtils == null)
        {
            logUtils = StructureMapConfig.Container.GetInstance<ILogUtils>();
        }

        var httpContext = HttpContext.Current;

        var actionDescriptor = actionExecutedContext.ActionContext.ActionDescriptor;

        var requestId = httpContext.Request.Headers.GetValues("X-RequestId").First();
        var userId = httpContext.User.Identity.GetUserId();
        var userName = httpContext.User.Identity.GetUserName();

        var responseContent = actionExecutedContext.Response.Content;
        if (responseContent == null)
        {
            logUtils.LogUsage($"RESPONSE LOG ipAddress:{httpContext.Request.UserHostAddress} requestId:{requestId} userId:{userId} userName:{userName} action:{actionDescriptor.ControllerDescriptor.ControllerName}.{actionDescriptor.ActionName} response:n/a");
        }
        else
        {
            var response = Task.Run(async () => await responseContent.ReadAsStringAsync()).Result;
            response = AesManager.EncryptData(response);
            logUtils.LogUsage($"RESPONSE LOG ipAddress:{httpContext.Request.UserHostAddress} requestId:{requestId} userId:{userId} userName:{userName} action:{actionDescriptor.ControllerDescriptor.ControllerName}.{actionDescriptor.ActionName} response:{response}");
        }
    }
}

Thanks for all the help, and I'm sorry that this little artifact caused the issue!

You need to login account before you can post.

About| Privacy statement| Terms of Service| Advertising| Contact us| Help| Sitemap|
Processed in 0.309379 second(s) , Gzip On .

© 2016 Powered by mzan.com design MATCHINFO