Tales from the .NET Migration Trenches - Authentication

Posts in this series:

Of all the topics in .NET migration, authentication, like always, is the one that is most characterized by "It Depends". The solution for addressing authentication is wholly dependent on what the current authentication solution is in the current .NET 4.8 application. If you're doing external SSO, then it's likely quite simple - the new solution is simply a new client for your external SSO.

In my situation, the .NET Framework application was responsible for authentication, i.e., it had a login screen. It was a home-grown identity provider, not using ASP.NET Identity. If you're using ASP.NET Identity and all the database backing stores, you're also looking at a data migration. I'll leave that as an exercise to the reader ;)

The end result we're looking for is:

  • Users can log in via one of the apps (.NET 8 or .NET 4.8)
  • Once logged in, both apps recognize the user as authenticated and can read identical claims/roles
  • Users can log out via one of the apps

Our two dumbed down options available to solving this are:

  • Remote authentication in ASP.NET 4.8
  • Cookie sharing between ASP.NET 4.8 and ASP.NET Core

The cookie sharing option is intriguing but it has some limitations:

  • Only works with Microsoft.Owin cookie authentication
  • Requires shared cookie and data protection configuration between applications

Our application didn't have that first constraint so we couldn't consider it. Remote authentication works by:

  • Users log in and out of the ASP.NET 4.8 application
  • ASP.NET Core adapters call APIs in ASP.NET 4.8 to retrieve user authentication information (claims) and populates its claims identity with this data

It's very similar to the remote session story:

Except getting we're getting the claims information from the ASP.NET application. This means, however, that the login/logout endpoints will need to be migrated last. Which means if our authentication story is complicated, we'll have plenty of runway since it'll be last.

Configuring Remote Authentication

Configuring remote authentication is straightforward if we've already added the remote app server for session. We add a single line of code to the ASP.NET application to AddAuthenticationServer:

this.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        options.RegisterKey<string>("FavoriteInstructor");
    })
    // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
    // ApiKey is a string representing a GUID
    .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"])
    .AddAuthenticationServer()
    .AddSessionServer();

And in our ASP.NET Core application, to add the authentication client:

builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        options.RegisterKey<string>("FavoriteInstructor");
    })
    .AddRemoteAppClient(options =>
    {
        // Provide the URL for the remote app that has enabled session querying
        options.RemoteAppUrl = new(builder.Configuration["ProxyTo"]);

        // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
        options.ApiKey = builder.Configuration["RemoteAppApiKey"];
    })
    .AddAuthenticationClient(true)
    .AddSessionClient();

There's a ton of options, because of course authentication is complicated, but this also means we can turn on authentication and authorization as normal in our ASP.NET Core application:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSystemWebAdapters();

With this in place, we can access all the normal ClaimsPrincipal and IIdentity details anywhere inside our ASP.NET Core application. We can't examine the security cookie - but we shouldn't anyway, our application code should only be concerned with the principal and identity, not the underlying details of how that got populated.

If we need to add more claims, those will get added on the ASP.NET side and automatically populated on the ASP.NET Core side with those API calls back to get all the claims for the user. It's another clever shim to allow us to migrate all controllers, actions, and application code that require authentication and authorization.

In the next post, we'll look at the middleware that exists in the ASP.NET application and migrate anything we actually want to migrate, and leave the rest behind.