DEV Community

Cover image for Share authentication cookies between ASP.NET 4.x and ASP.NET Core
1

Share authentication cookies between ASP.NET 4.x and ASP.NET Core

I've been working with ASP.NET 4.x and ASP.NET Core for a while, and I've always noticed some unusual differences between them.

The migration process from ASP.NET 4.x to ASP.NET Core can be challenging, depending on how the source code is designed or organized, the size of the application, and the packages used.

I'm going to present a technique for implementing a side-by-side migration process that ensures both applications continue to run.

Let's begin with a simple ASP.NET MVC 4.8 application configured for forms authentication.

Normally, the authentication cookie is created by calling FormsAuthentication.SetAuthCookie(username, rememberMe); after a successful login, and it is destroyed by calling FormsAuthentication.SignOut() upon logout. This setup is supported by a small configuration in the web.config file using the authentication tag.

<system.web>
  <compilation debug="true" targetFramework="4.8" />
  <httpRuntime targetFramework="4.8" />
  <authentication mode="Forms">
    <forms loginUrl="~/Account/Login" timeout="2880" requireSSL="true" path="/" />
  </authentication>
</system.web>
Enter fullscreen mode Exit fullscreen mode
using Mvc4FormAuthentication.Models;
using System.Web.Mvc;
using System.Web.Security;

namespace Mvc4FormAuthentication.Controllers
{
    public class AccountController : Controller
    {
        public ActionResult Login()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginViewModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            var isAuthenticated = IsAuthenticateUser(model.Username, model.Password);

            if (isAuthenticated)
            {
                // the AUTH cookie is set here
                FormsAuthentication.SetAuthCookie(model.Username, model.RememberMe);

                return Redirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
            }

            ModelState.AddModelError("", "Invalid username or password.");

            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Logout()
        {
            // the AUTH cookie is removed here
            FormsAuthentication.SignOut();

            return RedirectToAction("Index", "Home");
        }

        private bool IsAuthenticateUser(string username, string password)
        {
            return true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Using browser DevTools, you can locate the ASP.NET authentication cookie generated during the forms authentication login process.
ASPXAUTH Cookie

Easy!

However, to share the authentication cookie between the applications, we need to change the way this cookie is generated.

First, remove the web.config configuration. Let's go back to the original code without the authentication tag.

<system.web>
  <compilation debug="true" targetFramework="4.8" />
  <httpRuntime targetFramework="4.8" />
</system.web>
Enter fullscreen mode Exit fullscreen mode

Now, let's install some OWIN packages.

  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Interop
  • Microsoft.Owin.Security.Cookies

Next, create a startup class.

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Interop;
using Owin;
using System;
using System.IO;

[assembly: OwinStartup(typeof(Mvc4OwinAuthentication.Startup))]

namespace Mvc4OwinAuthentication
{
    public class Startup
    {
        const string APPLICATION_NAME = "SharedAuthCookieApp";
        const string COOKIE_NAME = ".AspNet.ApplicationAuthCookie";
        const string COOKIE_PATH = "/";
        const string SHARING_FOLDER_PATH = @"C:\Sources\Tests\DotNet\SharedAuthCookie";

        public void Configuration(IAppBuilder app)
        {
            var loginPath = new PathString("/Account/Login");

            var chunkingCookieManager = new ChunkingCookieManager();

            var dataProtectionProvider = DataProtectionProvider.Create(
                new DirectoryInfo(SHARING_FOLDER_PATH),
                builder =>
                {
                    builder.SetApplicationName(APPLICATION_NAME);
                }
            ).CreateProtector(
                "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                "Cookies",
                "v2"
            );

            var dataProtectorShim = new DataProtectorShim(dataProtectionProvider);

            var aspNetTicketDataFormat = new AspNetTicketDataFormat(dataProtectorShim);

            var cookieAuthenticationOptions = new CookieAuthenticationOptions
            {
                // This should match the AuthenticationType you use when creating the ClaimsIdentity ("Cookies")
                AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
                CookieHttpOnly = true,
                CookieManager = chunkingCookieManager,
                CookieName = COOKIE_NAME,
                CookiePath = COOKIE_PATH,
                CookieSameSite = SameSiteMode.Lax,
                ExpireTimeSpan = TimeSpan.FromMinutes(20),
                LoginPath = loginPath,
                SlidingExpiration = true,
                TicketDataFormat = aspNetTicketDataFormat
            };

            app.UseCookieAuthentication(cookieAuthenticationOptions);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Add a configuration to AntiForgeryToken on Global.asax in the Application_Start.

protected void Application_Start()
{
    AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}
Enter fullscreen mode Exit fullscreen mode

And finally, the AccountController should create the auth cookie.

using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Mvc4OwinAuthentication.Models;
using System.Collections.Generic;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;

namespace Mvc4OwinAuthentication.Controllers
{
    public class AccountController : Controller
    {
        public ActionResult Login()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginViewModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            var user = AuthenticateUser(model.Username, model.Password);
            if (user != null)
            {
                var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                    new Claim(ClaimTypes.Name, user.Username)
                };

                var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);

                var authManager = HttpContext.GetOwinContext().Authentication;

                var authProperties = new AuthenticationProperties
                {
                    IsPersistent = model.RememberMe
                };

                authManager.SignIn(authProperties, claimsIdentity);

                return Redirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
            }

            ModelState.AddModelError("", "Invalid username or password.");
            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Logout()
        {
            HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
            return RedirectToAction("Index", "Home");
        }

        private User AuthenticateUser(string username, string password)
        {
            return new User(username);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here's our new cookie.

OWIN ASP AUTH Cookie

😎

Let's dive into the ASP.NET Core 9 part.

In Program.cs, configure the authentication to use cookies.

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;

namespace MvcCoreAuthentication
{
    public class Program
    {
        const string APPLICATION_NAME = "SharedAuthCookieApp";
        const string COOKIE_NAME = ".AspNet.ApplicationAuthCookie";
        const string COOKIE_PATH = "/";
        const string LOGIN_PATH = "/Account/Login";
        const string SHARING_FOLDER_PATH = @"C:\Sources\Tests\DotNet\SharedAuthCookie";

        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();


            #region Cookie Configuration

            builder.Services.AddDataProtection()
                .PersistKeysToFileSystem(new DirectoryInfo(SHARING_FOLDER_PATH))
                .SetApplicationName(APPLICATION_NAME);

            var chunkingCookieManager = new ChunkingCookieManager();

            builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.Cookie.HttpOnly = true;
                    options.Cookie.Name = COOKIE_NAME;
                    options.Cookie.Path = COOKIE_PATH;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.CookieManager = chunkingCookieManager;
                    options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
                    options.LoginPath = LOGIN_PATH;
                    options.SlidingExpiration = true;
                });

            #endregion


            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseRouting();

            app.UseAuthentication(); // Cookie Configuration - Add this line
            app.UseAuthorization();

            app.MapStaticAssets();
            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}")
                .WithStaticAssets();

            app.Run();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here's our ASP.NET Core cookie 😉

ASP NET CORE Cookie

It's time to see if everything worked.

Create a new controller with an action decorated with the [Authorize] attribute (do this in both applications).

public class PrivateController : Controller
{
    [Authorize]
    public ActionResult Index()
    {
        return View();
    }
}
Enter fullscreen mode Exit fullscreen mode

Update the _Layout.cshtml file to link the routes in both apps.

ASP.NET 4.8

<ul class="navbar-nav flex-grow-1">
    @if (Request.IsAuthenticated)
    {
        <li>
            <a href="#">Hello, @User.Identity.Name!</a>
        </li>
        <li>
            @using (Html.BeginForm("Logout", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-form" }))
            {
                @Html.AntiForgeryToken()
                <button type="submit" class="btn btn-link navbar-btn">Logout</button>
            }
        </li>
    }
    else
    {
        <li>
            @Html.ActionLink("Login", "Login", "Account", new { area = "" }, new { @class = "nav-link" })
        </li>
    }

    <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, new { @class = "nav-link" })</li>
    <li>@Html.ActionLink("Local Private", "Index", "Private", new { area = "" }, new { @class = "nav-link" })</li>
    <li>
        <a class="nav-link" href="https://localhost:7102/Private">External Private</a>
    </li>
</ul>
Enter fullscreen mode Exit fullscreen mode

ASP.NET Core

<ul class="navbar-nav flex-grow-1">
    @if (User.Identity.IsAuthenticated)
    {
        <li class="nav-item">
            <span class="navbar-text">Hello, @User.Identity.Name!</span>
        </li>
        <li class="nav-item">
            <form id="logoutForm" asp-controller="Account" asp-action="Logout" method="post" class="form-inline">
                @Html.AntiForgeryToken()
                <button type="submit" class="nav-link btn btn-link" style="display:inline; padding:0;">Logout</button>
            </form>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link" asp-controller="Account" asp-action="Login">Login</a>
        </li>
    }

    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Private" asp-action="Index">Local Private</a>
    </li>
    <li>
        <a class="nav-link text-dark" href="https://localhost:44301/Private">External Private</a>
    </li>
</ul>
Enter fullscreen mode Exit fullscreen mode

We're done! ⭐

Bonus

Of course, sharing the DataProtectionKey via the local file system isn't ideal.

There are many remote and distributed alternatives available, but for demonstration purposes, we'll use Valkey/Redis.

Now, let's create and run a container with Valkey to store the data protection key.

docker run -d --rm -p "6379:6379" --name share-valkey valkey/valkey:alpine
Enter fullscreen mode Exit fullscreen mode

(it could be any Valkey/Redis server that you have access)

Back to ASP.NET 4.8 application

Install the package Microsoft.AspNetCore.DataProtection.StackExchangeRedis

And change the Startup class

public class Startup
{
    const string APPLICATION_NAME = "SharedAuthCookieApp";
    const string COOKIE_NAME = ".AspNet.ApplicationAuthCookie";
    const string COOKIE_PATH = "/";
    const string LOGIN_PATH = "/Account/Login";
    const string SHARING_FOLDER_PATH = @"C:\Sources\Tests\DotNet\SharedAuthCookie";
    const string DATA_PROTECTION_KEY_CACHE_NAME = "MY-SHARED-DATA-PROTECTION-KEY";

    public void Configuration(IAppBuilder app)
    {
        var loginPath = new PathString(LOGIN_PATH);

        var chunkingCookieManager = new ChunkingCookieManager();

        var dataProtectionProvider = DataProtectionProvider.Create(
            new DirectoryInfo(SHARING_FOLDER_PATH),
            builder =>
            {
                builder.SetApplicationName(APPLICATION_NAME);
                builder.PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect("localhost:6379"), DATA_PROTECTION_KEY_CACHE_NAME);
            }
        ).CreateProtector(
            "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
            "Cookies",
            "v2"
        );
Enter fullscreen mode Exit fullscreen mode

Don't worry about maintaining the DirectoryInfo because the builder won't generate the DataProtectionKey file anymore.

And in the ASP.NET Core application, configure Data Protection to use the same remote key storage.

Install the package Microsoft.AspNetCore.DataProtection.StackExchangeRedis

Change the Program class

public class Program
{
    const string APPLICATION_NAME = "SharedAuthCookieApp";
    const string COOKIE_NAME = ".AspNet.ApplicationAuthCookie";
    const string COOKIE_PATH = "/";
    const string LOGIN_PATH = "/Account/Login";
    const string DATA_PROTECTION_KEY_CACHE_NAME = "MY-SHARED-DATA-PROTECTION-KEY";

    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddControllersWithViews();


        #region Cookie Configuration

        builder.Services.AddDataProtection()
            .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect("localhost:6379"), DATA_PROTECTION_KEY_CACHE_NAME)
            .SetApplicationName(APPLICATION_NAME);
Enter fullscreen mode Exit fullscreen mode

Running the applications, we can see that the DataProtectionKey is being used from Valkey/Redis.

DataProtectionKey on Valkey

😍

That's it!

I know there's a lot of code, but in the end, it's pretty straightforward once you understand how to configure the OWIN cookie.

Let's review some key points.

In ASP.NET 4.8

  • Install the following packages:
    • Microsoft.Owin.Host.SystemWeb
    • Microsoft.Owin.Security.Cookies
    • Microsoft.Owin.Security.Interop
    • Microsoft.AspNetCore.DataProtection.StackExchangeRedis
  • Create the Startup class to configure the cookie settings
  • Update the AccountController to properly create and destroy the authentication cookie
  • Configure AntiForgeryToken in the Application_Start method of Global.asax

In ASP.NET Core

  • Install the package:
    • Microsoft.AspNetCore.DataProtection.StackExchangeRedis
  • Configure Cookie Authentication:
    • Add cookie authentication to the WebApplicationBuilder.
    • Call app.UseAuthentication() in the pipeline.
  • Modify how the Account controller creates and destroys the authentication cookie

Important to note

  • The cookie parameters must be the same in both applications.
  • Do not expose the DataProtectionKey 🔥

The source code can be found here

Thanks, and keep making awesome code!

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay