Migrating Authentication and Identity to ASP.NET Core 2.0

By Scott Addie and Hao Kung

ASP.NET Core 2.0 has a new model for authentication and Identity which simplifies configuration by using services. ASP.NET Core 1.x applications that use authentication or Identity can be updated to use the new model as outlined below.

Authentication Middleware and Services

In 1.x projects, authentication is configured via middleware. A middleware method is invoked for each authentication scheme you want to support.

The following 1.x example configures Facebook authentication with Identity in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
    app.UseIdentity();
    app.UseFacebookAuthentication(new FacebookOptions { 
        AppId = Configuration["auth:facebook:appid"],
        AppSecret = Configuration["auth:facebook:appsecret"]
    });
} 

In 2.0 projects, authentication is configured via services. Each authentication scheme is registered in the ConfigureServices method of Startup.cs. The UseIdentity method is replaced with UseAuthentication.

The following 2.0 example configures Facebook authentication with Identity in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

    // If you want to tweak Identity cookies, they're no longer part of IdentityOptions.
    services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
    services.AddAuthentication()
            .AddFacebook(options => {
                options.AppId = Configuration["auth:facebook:appid"];
                options.AppSecret = Configuration["auth:facebook:appsecret"];
            });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseAuthentication();
}

The UseAuthentication method adds a single authentication middleware component which is responsible for automatic authentication and the handling of remote authentication requests. It replaces all of the individual middleware components with a single, common middleware component.

Below are 2.0 migration instructions for each major authentication scheme.

Select one of the two options below, and make the necessary changes in Startup.cs:

  1. Use cookies with Identity

    • Replace UseIdentity with UseAuthentication in the Configure method:

      app.UseAuthentication();
      
    • Invoke the AddIdentity method in the ConfigureServices method to add the cookie authentication services.

    • Optionally, invoke the ConfigureApplicationCookie or ConfigureExternalCookie method in the ConfigureServices method to tweak the Identity cookie settings.

      services.AddIdentity<ApplicationUser, IdentityRole>()
              .AddEntityFrameworkStores<ApplicationDbContext>()
              .AddDefaultTokenProviders();
      
      services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
      
  2. Use cookies without Identity

    • Replace the UseCookieAuthentication method call in the Configure method with UseAuthentication:

      app.UseAuthentication();
      
    • Invoke the AddAuthentication and AddCookie methods in the ConfigureServices method:

      // If you don't want the cookie to be automatically authenticated and assigned to HttpContext.User, 
      // remove the CookieAuthenticationDefaults.AuthenticationScheme parameter passed to AddAuthentication.
      services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
              .AddCookie(options => {
                  options.LoginPath = "/Account/LogIn";
                  options.LogoutPath = "/Account/LogOff";
              });
      

JWT Bearer Authentication

Make the following changes in Startup.cs:

  • Replace the UseJwtBearerAuthentication method call in the Configure method with UseAuthentication:

    app.UseAuthentication();
    
  • Invoke the AddJwtBearer method in the ConfigureServices method:

    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options => {
                options.Audience = "http://localhost:5001/";
                options.Authority = "http://localhost:5000/";
            });
    

    This code snippet doesn't use Identity, so the default scheme should be set by passing JwtBearerDefaults.AuthenticationScheme to the AddAuthentication method.

OpenID Connect (OIDC) Authentication

Make the following changes in Startup.cs:

  • Replace the UseOpenIdConnectAuthentication method call in the Configure method with UseAuthentication:

    app.UseAuthentication();
    
  • Invoke the AddOpenIdConnect method in the ConfigureServices method:

    services.AddAuthentication(options => {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options => {
        options.Authority = Configuration["auth:oidc:authority"];
        options.ClientId = Configuration["auth:oidc:clientid"];
    });
    

Facebook Authentication

Make the following changes in Startup.cs:

  • Replace the UseFacebookAuthentication method call in the Configure method with UseAuthentication:

    app.UseAuthentication();
    
  • Invoke the AddFacebook method in the ConfigureServices method:

    services.AddAuthentication()
            .AddFacebook(options => {
                options.AppId = Configuration["auth:facebook:appid"];
                options.AppSecret = Configuration["auth:facebook:appsecret"];
            });
    

Google Authentication

Make the following changes in Startup.cs:

  • Replace the UseGoogleAuthentication method call in the Configure method with UseAuthentication:

    app.UseAuthentication();
    
  • Invoke the AddGoogle method in the ConfigureServices method:

    services.AddAuthentication()
            .AddGoogle(options => {
                options.ClientId = Configuration["auth:google:clientid"];
                options.ClientSecret = Configuration["auth:google:clientsecret"];
            });    
    

Microsoft Account Authentication

Make the following changes in Startup.cs:

  • Replace the UseMicrosoftAccountAuthentication method call in the Configure method with UseAuthentication:

    app.UseAuthentication();
    
  • Invoke the AddMicrosoftAccount method in the ConfigureServices method:

    services.AddAuthentication()
            .AddMicrosoftAccount(options => {
                options.ClientId = Configuration["auth:microsoft:clientid"];
                options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
            });
    

Twitter Authentication

Make the following changes in Startup.cs:

  • Replace the UseTwitterAuthentication method call in the Configure method with UseAuthentication:

    app.UseAuthentication();
    
  • Invoke the AddTwitter method in the ConfigureServices method:

    services.AddAuthentication()
            .AddTwitter(options => {
                options.ConsumerKey = Configuration["auth:twitter:consumerkey"];
                options.ConsumerSecret = Configuration["auth:twitter:consumersecret"];
            });
    

Setting Default Authentication Schemes

In 1.x, the AutomaticAuthenticate and AutomaticChallenge properties were intended to be set on a single authentication scheme. There was no good way to enforce this.

In 2.0, these two properties have been removed as flags on the individual AuthenticationOptions instance and have moved into the base AuthenticationOptions class. The properties can be configured in the AddAuthentication method call within the ConfigureServices method of Startup.cs:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

Alternatively, use an overloaded version of the AddAuthentication method to set more than one property. In the following overloaded method example, the default scheme is set to CookieAuthenticationDefaults.AuthenticationScheme. The authentication scheme may alternatively be specified within your individual [Authorize] attributes or authorization policies.

services.AddAuthentication(options => {
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});

Define a default scheme in 2.0 if one of the following conditions is true:

  • You want the user to be automatically signed in
  • You use the [Authorize] attribute or authorization policies without specifying schemes

An exception to this rule is the AddIdentity method. This method adds cookies for you and sets the default authenticate and challenge schemes to the application cookie IdentityConstants.ApplicationScheme. Additionally, it sets the default sign-in scheme to the external cookie IdentityConstants.ExternalScheme.

Use HttpContext Authentication Extensions

The IAuthenticationManager interface is the main entry point into the 1.x authentication system. It has been replaced with a new set of HttpContext extension methods in the Microsoft.AspNetCore.Authentication namespace.

For example, 1.x projects reference an Authentication property:

// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);

In 2.0 projects, import the Microsoft.AspNetCore.Authentication namespace, and delete the Authentication property references:

// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

Windows Authentication (HTTP.sys / IISIntegration)

There are two variations of Windows authentication:

  1. The host only allows authenticated users
  2. The host allows both anonymous and authenticated users

The first variation described above is unaffected by the 2.0 changes.

The second variation described above is affected by the 2.0 changes. As an example, you may be allowing anonymous users into your application at the IIS or HTTP.sys layer but authorizing users at the Controller level. In this scenario, set the default scheme to IISDefaults.AuthenticationScheme in the ConfigureServices method of Startup.cs:

services.AddAuthentication(IISDefaults.AuthenticationScheme);

Failure to set the default scheme accordingly prevents the authorize request to challenge from working.

IdentityCookieOptions Instances

A side effect of the 2.0 changes is the switch to using named options instead of cookie options instances. The ability to customize the Identity cookie scheme names is removed.

For example, 1.x projects use constructor injection to pass an IdentityCookieOptions parameter into AccountController.cs. The external cookie authentication scheme is accessed from the provided instance:

public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IOptions<IdentityCookieOptions> identityCookieOptions,
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILoggerFactory loggerFactory)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
    _emailSender = emailSender;
    _smsSender = smsSender;
    _logger = loggerFactory.CreateLogger<AccountController>();
}

The aforementioned constructor injection becomes unnecessary in 2.0 projects, and the _externalCookieScheme field can be deleted:

public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILoggerFactory loggerFactory)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _emailSender = emailSender;
    _smsSender = smsSender;
    _logger = loggerFactory.CreateLogger<AccountController>();
}

The IdentityConstants.ExternalScheme constant can be used directly:

// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

Add IdentityUser POCO Navigation Properties

The Entity Framework (EF) Core navigation properties of the base IdentityUser POCO (Plain Old CLR Object) have been removed. If your 1.x project used these properties, manually add them back to the 2.0 project:

/// <summary>
/// Navigation property for the roles this user belongs to.
/// </summary>
public virtual ICollection<IdentityUserRole<int>> Roles { get; } = new List<IdentityUserRole<int>>();

/// <summary>
/// Navigation property for the claims this user possesses.
/// </summary>
public virtual ICollection<IdentityUserClaim<int>> Claims { get; } = new List<IdentityUserClaim<int>>();

/// <summary>
/// Navigation property for this users login accounts.
/// </summary>
public virtual ICollection<IdentityUserLogin<int>> Logins { get; } = new List<IdentityUserLogin<int>>();

To prevent duplicate foreign keys when running EF Core Migrations, add the following to your IdentityDbContext class' OnModelCreating method (after the base.OnModelCreating(); call):

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    // Customize the ASP.NET Identity model and override the defaults if needed.
    // For example, you can rename the ASP.NET Identity table names and more.
    // Add your customizations after calling base.OnModelCreating(builder);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Claims)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Logins)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Roles)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);
}

Replace GetExternalAuthenticationSchemes

The synchronous method GetExternalAuthenticationSchemes was removed in favor of an asynchronous version. 1.x projects have the following code in ManageController.cs:

var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList();

This method appears in Login.cshtml too:

var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList();
        <div>
            <p>
                @foreach (var provider in loginProviders)
                {
                    <button type="submit" class="btn btn-default" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
                }
            </p>
        </div>
    </form>
}

In 2.0 projects, use the GetExternalAuthenticationSchemesAsync method:

var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync();
var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();

In Login.cshtml, the AuthenticationScheme property accessed in the foreach loop changes to Name:

var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        <div>
            <p>
                @foreach (var provider in loginProviders)
                {
                    <button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                }
            </p>
        </div>
    </form>
}

ManageLoginsViewModel Property Change

A ManageLoginsViewModel object is used in the ManageLogins action of ManageController.cs. In 1.x projects, the object's OtherLogins property return type is IList<AuthenticationDescription>. This return type requires an import of Microsoft.AspNetCore.Http.Authentication:

using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore1App.Models.ManageViewModels
{
    public class ManageLoginsViewModel
    {
        public IList<UserLoginInfo> CurrentLogins { get; set; }

        public IList<AuthenticationDescription> OtherLogins { get; set; }
    }
}

In 2.0 projects, the return type changes to IList<AuthenticationScheme>. This new return type requires replacing the Microsoft.AspNetCore.Http.Authentication import with a Microsoft.AspNetCore.Authentication import.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore2App.Models.ManageViewModels
{
    public class ManageLoginsViewModel
    {
        public IList<UserLoginInfo> CurrentLogins { get; set; }

        public IList<AuthenticationScheme> OtherLogins { get; set; }
    }
}

Additional Resources

For additional details and discussion, see the Discussion for Auth 2.0 issue on GitHub.