When a session stops working because you're being too strict...

Recently, I had to pass around some data between two HTTP requests in an ASP.NET Core project. Because this data could be interesting to retain during the user's session, I stored it in HttpContext.Session which was configured in the Startup class anyway. During normal web app navigation events (going to a form, POSTing the form, ...), that session data worked as expected. Also when a RedirectToAction was triggered in a controller while running on localhost, the session was properly updated and preserved.

Time to create a new build and deploy it to the DEV environment, right? Well...

While testing in the DEV environment, everything still worked except when we hit that RedirectToAction flow: the session state was being reset every time! I doublechecked all of the settings, but didn't see any obvious issues there. Then I tried to use TempData as a fallback for the RedirectToAction flow, which fixed the issue on the remote environment.

Huh. But TempData, just like Session, also uses cookies... Why does TempData work then when Session doesn't? Is it a difference in implementation, or the positioning of the middleware?

TempData, by default, uses its own cookie. You can also specify this behavior manually or change the cookie settings by calling AddCookieTempDataProvider:

services.AddControllersWithViews()
    .AddCookieTempDataProvider();

You can also tell ASP.NET Core that you want TempData to use Session if you're already using Session anyway, to reduce the amount of cookies used and to rely on the distributed cache when running your web application on multiple machines. To do this, simply replace the AddCookieTempDataProvider call with AddSessionStateTempDataProvider. You don't (or can't) provide any configuration to this method, because the configuration is being done when adding session support:

services.AddControllersWithViews()
    .AddSessionStateTempDataProvider();

services.AddSession(x =>
{
    x.Cookie.HttpOnly = true;
    x.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    x.Cookie.SameSite = SameSiteMode.Strict;
    x.Cookie.Name = Configuration["CookieNames:Session"];
});

This is how we configured the session's cookie:

  • HTTP Only, because we don't want any JavaScript to access the session cookie
  • SecurePolicy set to Always, because we use HTTPS all the way
  • SameSite set to Strict, because we don't want this cookie to be sent when a request is initiated by a third-party
  • And finally, we change the name to be something less obvious.

Because TempData worked, and we already had Session enabled, I tried using the SessionStateTempDataProvider. Guess what? TempData stopped working between redirects.

Back to the investigation. What's the difference between using session to store TempData and using the original CookieTempDataProvider? Well, here's the source code of CookieTempDataProviderOptions :

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace Microsoft.AspNetCore.Mvc;

/// <summary>
/// Provides programmatic configuration for cookies set by <see cref="CookieTempDataProvider"/>
/// </summary>
public class CookieTempDataProviderOptions
{
    private CookieBuilder _cookieBuilder = new CookieBuilder
    {
        Name = CookieTempDataProvider.CookieName,
        HttpOnly = true,

        // Check the comment on CookieBuilder below for more details
        SameSite = SameSiteMode.Lax,

        // This cookie has been marked as non-essential because a user could use the SessionStateTempDataProvider,
        // which is more common in production scenarios. Check the comment on CookieBuilder below
        // for more information.
        IsEssential = false,

        // Some browsers do not allow non-secure endpoints to set cookies with a 'secure' flag or overwrite cookies
        // whose 'secure' flag is set (http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-alone.html).
        // Since mixing secure and non-secure endpoints is a common scenario in applications, we are relaxing the
        // restriction on secure policy on some cookies by setting to 'None'. Cookies related to authentication or
        // authorization use a stronger policy than 'None'.
        SecurePolicy = CookieSecurePolicy.None,
    };

    /// <summary>
    /// <para>
    /// Determines the settings used to create the cookie in <see cref="CookieTempDataProvider"/>.
    /// </para>
    /// <para>
    /// <see cref="CookieBuilder.SameSite"/> defaults to <see cref="SameSiteMode.Lax"/>. Setting this to
    /// <see cref="SameSiteMode.Strict"/> may cause browsers to not send back the cookie to the server in an
    /// OAuth login flow.
    /// <see cref="CookieBuilder.SecurePolicy"/> defaults to <see cref="CookieSecurePolicy.SameAsRequest" />.
    /// <see cref="CookieBuilder.HttpOnly"/> defaults to <c>true</c>.
    /// <see cref="CookieBuilder.IsEssential"/> defaults to <c>false</c>, This property is only considered when a
    /// user opts into the CookiePolicyMiddleware. If you are using this middleware and want to use
    /// <see cref="CookieTempDataProvider"/>, then either set this property to <c>true</c> or
    /// request user consent for non-essential cookies.
    /// </para>
    /// </summary>
    public CookieBuilder Cookie
    {
        get => _cookieBuilder;
        set => _cookieBuilder = value ?? throw new ArgumentNullException(nameof(value));
    }
}

Did you spot it? Apparently, there's a bug in browsers (yes, I call this a bug), that causes browsers to not send a cookie back to the server in OAuth login flows when the SameSite is set to Strict. And during OAuth login flows, a lot of redirects occur.

With this new knowledge, I tried relaxing the settings of the session cookie by setting x.Cookie.SameSite = SameSiteMode.Lax; and deployed this attempt to the server.

It worked! TempData survived the redirect, together with the Session!

So, should you find yourself in a situation where something cookie-related works locally but not remotely, and that cookie uses SameSiteMode.Strict? Then first try to relax it a little before trying anything else.

Did you find this article valuable?

Support Wesley Cabus by becoming a sponsor. Any amount is appreciated!