While migrating a ASP.NET Core 1.1 app (running on .NET Framework 4.6.1) to ASP.NET Core 2.0 (netcoreapp2.0) I needed to migrate some custom authentication. There are tons of guides on the internet on how to migrate authentication if you are using one of the standard once like JwtBearer, Facebook or Google. If not, there isn't a ton of info avaiable and the info that is there is spread accross multiple Github issues and questions in comments on docs.microsoft.com.

Note: If you came here looking for how to setup Facebook, Google etc, have a look at the official Microsoft docs: https://docs.microsoft.com/en-us/aspnet/core/security/

Summary / TL;DR

  • Authentication schemes are added as services
  • A single middleware is now used for authentication instead of several as in ASP.NET Core 1.1
  • Options object to pass variables and services to the auth scheme

ASP.NET Core 1.1

Our system consisted of two different types of authentication. "Shared Key" for write access and "public token" for read only access. The two are implemented as middlewares inheriting from AuthenticationMiddleware. Each of these middlewares are only available for some specific controllers in the system.

Startup.cs

public void ConfigureServices(IServiceCollection services) 
{
    // Other setup omitted
    services.AddAuthentication();
    services.AddAuthorization(
        options =>
        {
            // Policy building omitted
        });
}

 public void Configure(IHostingEnvironment env, IApplicationBuilder app) 
 {
     // Other setup omitted
     app.UseSharedKeyAuthentication();
     app.UseTokenAuthentication();
 }

In ASP.NET Core 1.1 the authentication handlers will be called sequentually for every request if the former handler returns AuthenticateResult.Skip(); from the HandleAuthenticateAsync method. Each of the UseXXXAuthentication registers each middleware to the application builder, e.g:

public static IApplicationBuilder UseSharedKeyAuthentication(this IApplicationBuilder app)
{
    if (app == null)
    {
        throw new ArgumentNullException(nameof(app));
    }

    return app.UseMiddleware<SharedKeyAuthenticationMiddleware>(Options.Create(new SharedKeyAuthenticationOptions()));
}

SharedKeyAuthenticationOptions inherit from AuthenticationOptions and set the base class properties as below. AutomaticChallenge and AutomaticAuthenticate is used to run this handler for every request. AuthenticationScheme says which scheme the handler handles.

public class SharedKeyAuthenticationOptions : AuthenticationOptions
    {
        public SharedKeyAuthenticationOptions()
        {
            AutomaticAuthenticate = true;
            AutomaticChallenge = true;
            AuthenticationScheme = "SharedKey";
        }
    }

The setup for UseTokenAuthentication is similar.

The SharedKeyAuthenticationMiddleware inherits from AuthenticationMiddleware<SharedKeyAuthenticationOptions> and simply overrides the CreateHandler method to create a new SharedKeyAuthenticationMiddleware:

protected override AuthenticationHandler<SharedKeyAuthenticationOptions> CreateHandler()
{
    return new SharedKeyAuthenticationHandler(_sharedKeyAuthenticationProcess, _serviceUserRepository, _logger);
}

Dependency injection works out of the box to the middleware which in turn passes the injected objects to the handler.

ASP.NET Core 2.0

In 2.0 there is just a single middleware handling all authentication and instead handlers are registered as services.

Startup.cs

services.AddAuthorization(Policies.Configure);

public void ConfigureServices(IServiceCollection services) 
{
    // No default schema. Schema is set per policy.
    services.AddAuthentication()
            .AddSharedSecret(services)
            .AddPublicToken(services);
}

public void Configure(IHostingEnvironment env, IApplicationBuilder app) 
{
    app.UseAuthentication();
}

And done! No, not really... there are some more stuff that needs to be done.

AddSharedSecret and AddPublicToken registers a scheme to the AuthenticationBuilder class returned by AddAuthentication.

AddSharedSecret looks like this:

return builder.AddScheme<SharedKeyAuthenticationOptions, SharedKeyAuthenticationHandler>(
                "SharedKey", // Name of scheme
                "SharedKey", // Display name of scheme
                options =>
                {
                    var provider = services.BuildServiceProvider();
                    // Logger, ServiceUserRepository and SharedKeyAuthenticationProcess are all things that were injected into the custom authentication
                    // middleware in ASP.NET Core 1.1. This is now added to the options object instead.
                    options.Logger = provider.GetService<global::Serilog.ILogger>();
                    options.ServiceUserRepository = provider.GetService<IServiceUserRepository>();
                    options.SharedKeyAuthenticationProcess = provider.GetService<ISharedKeyAuthenticationProcess>();
                });

The SharedKeyAuthenticationOptions no longer needs to specify AutomaticChallenge, AutomaticAuthenticate or AuthenticationScheme. The first two are obsolete in 2.0 and the latter is handled by the AuthenticationBuilder. SharedKeyAuthenticationOptions now looks like below. The Validate method can be overridden to add custom validation to the options object. Any exception thrown causes the validation to fail.

public class SharedKeyAuthenticationOptions : AuthenticationSchemeOptions
    {
        public static string AuthenticationScheme => "SharedKey";

        public ISharedKeyAuthenticationProcess SharedKeyAuthenticationProcess { get; set; }

        public IServiceUserRepository ServiceUserRepository { get; set; }

        public global::Serilog.ILogger Logger { get; set; }

        public override void Validate()
        {
            if (SharedKeyAuthenticationProcess == null)
                throw new NullReferenceException($"{nameof(SharedKeyAuthenticationProcess)} is null");

            if (ServiceUserRepository == null)
                throw new NullReferenceException($"{nameof(ServiceUserRepository)} is null");

            if (Logger == null)
                throw new NullReferenceException($"{nameof(Logger)} is null");
        }
    }

There are minimal changes required to SharedKeyAuthenticationHandler:

Constructor must look like this, otherwise an exception will be thrown:

public SharedKeyAuthenticationHandler(IOptionsMonitor<SharedKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

All references to the fields we previously used needs to be replaced with references to the Options object, e.g:

ASP.NET Core 1.1 -> _logger.WriteEvent(...);

ASP.NET Core 2.0 _> Options.Logger.WriteEvent(...);

AuthenticateResult.Skip() has been renamed to AuthenticateResult.NoResult(). Note that returning NoResult will NOT call the next handler in the chain.

The above line is important! You can by default only address a single authentication schema in ASP.NET Core 2.0 with the built in authentication middleware. To add multiple schemas you need to specify all schemas supported on each [Authorize] attribute or on each policy (or use a custom middleware). If you only have a single scheme for authentication you can specify this as default in services.AddAuthentication

services.AddAuthentication(
                        options =>
                        {
                            options.DefaultScheme = "SharedKey";
                        })
                    .AddSharedSecret(services);

Since we are using claims based authentication I added the different schemas to our policy builder as each policy is only accessible using one authentication schema.

Example code:

public void ConfigureServices(IServiceCollection services) 
{
services.AddAuthorization(
    options =>
    {
        options.AddPolicy("SomePolicy",
            builder =>
            {
                builder.RequireClaim("MyClaim");
                // Multiple authentication schemes can be added and the output from them will then be merged into a single identity.
                builder.AddAuthenticationSchemes("SharedKey"); // Runs SharedKeyAuthenticationHandler
                // builder.AddAuthenticationSchemes("Token"); // Runs TokenAuthenticationHandler
            });
    });
}

1 Comment