Andre Lombaard
Andre Lombaard

Reputation: 7105

Write Custom Authentication classes for ASP.NET 5

The out of the box ASP.NET 5 Identity classes are straight forward and it is easy to setup and use, my problem however is that I'm working with a legacy system with existing user and permission tables that I have to customize the Identity system for. The Identity system seems very pluggable, but I can't find any proper documentation on how to write custom Identity classes. Is there any documentation on

  1. How to write custom ASP.NET 5 Identity classes that can cater for legacy system user and permission tables
  2. How do I setup the use of the custom Identity classes for use in a ASP.NET 5, MVC 6 application?

Upvotes: 2

Views: 639

Answers (1)

Andre Lombaard
Andre Lombaard

Reputation: 7105

There is not a lot of documentation available yet, so I played around with the latest Identity classes which is currently Microsoft.AspNet.Identity.EntityFramework 3.0.0-rc1-final and came up with a solution that is working with my legacy user database tables.

First of all make sure that your legacy user entity class implement the IdentityUser class so that we can use the class for authentication within ASP.NET 5

public class MyLegacyUser : IdentityUser
{
    // Your MyLegacyUser properties will go here as usual
}

Make sure that you ignore any properties inherited from the IdentityUser class that you would not like to use (Those are the properties not contained within your user table). We do this by using the fluent api within the OnModelCreating method of the DbContext class.

public class MyDbContext : DbContext
{
    public DbSet<MyLegacyUser> MyLegacyUser { get; set; }

    // For simplicity I will add only the OnModelCreating method here
    protected override void OnModelCreating
    {
        modelBuilder.Entity<MyLegacyUser>(entity =>
        {
            entity.Ignore(e => e.AccessFailedCount);
            entity.Ignore(e => e.Claims);
            entity.Ignore(e => e.ConcurrencyStamp);
            entity.Ignore(e => e.Email);
            entity.Ignore(e => e.EmailConfirmed);
            entity.Ignore(e => e.Id);
            entity.Ignore(e => e.LockoutEnabled);
            entity.Ignore(e => e.LockoutEnd);
            entity.Ignore(e => e.Logins);
            entity.Ignore(e => e.NormalizedEmail);
            entity.Ignore(e => e.NormalizedUserName);
            entity.Ignore(e => e.PasswordHash);
            entity.Ignore(e => e.PhoneNumber);
            entity.Ignore(e => e.PhoneNumberConfirmed);
            entity.Ignore(e => e.Roles);
            entity.Ignore(e => e.SecurityStamp);
            entity.Ignore(e => e.TwoFactorEnabled);
        }
    }
}

Now we have to implement our own custom UserManager class to authenticate with our legacy user. Make sure that your new class implement UserManager<T>, where T is your MyLegacyUser. Once this is done override the CheckPasswordAsync to authenticate your user.

Note: The CheckPasswordAsync method is not responsible for returning an authenticated user, it is simply a method that will return true or false to indicate if the user was successfully authenticated. The authenticated user is set by another class which I will explain below.

public class MyLegacyUserManager : UserManager<MyLegacyUser>
{
    public WorldUserManager(IUserStore<MasterUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<MasterUser> passwordHasher, IEnumerable<IUserValidator<MasterUser>> userValidators, IEnumerable<IPasswordValidator<MasterUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<MasterUser>> logger, IHttpContextAccessor contextAccessor) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, contextAccessor)
    {
    }

    public override async Task<bool> CheckPasswordAsync(MasterUser user, string password)
    {
        // This is my own authentication manager class that handles user authentication
       // Add your own code to authenticate your user here
        return new AuthenticationManager().Authenticate(user.EmailAddress, password);
    }
}    

Once this is done we have to implement our own UserStore class. There are a few interfaces you can implement such as IUserStore<T>, IUserLoginStore<T>, IUserClaimsStore<T> etc. I implemented the IUserClaimsStore<T> interface and implemented the GetUserIdAsync, GetUserNameAsync, FindByIdAsync and GetClaimsAsync methods

public class MyLegacyUserClaimStore : IUserClaimStore<MyLegacyUser>
{
    // Here I simply returned the username of the user parameter I recieved as input
    public Task<string> GetUserIdAsync(MasterUser user, CancellationToken cancellationToken)
    {
        return Task.Run(() => user.UserName, cancellationToken);
    }
}

// Here I simply returned the username of the user parameter I recieved as input
public Task<string> GetUserNameAsync(MasterUser user, CancellationToken cancellationToken)
{
    return Task.Run(() => user.UserName, cancellationToken);
}

public Task<MasterUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
    // This is my manager class to read my user for the userId
    // Add your own code to read the user for the set Id here
    return Task.Run(() => new MyLegacyUserUserManager().ReadForEmailAddress(userId, 0, true, true), cancellationToken);
}

 public Task<MasterUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
 {
     // This is my manager class to read my user for the normalizedUserName
    // Add your own code to read the user for the set normalizedUserName here
     return Task.Run(() => new MyLegacyUserManager().ReadForEmailAddress(normalizedUserName, 0, true, true), cancellationToken);
 }

// If you want to make use of Claims make sure that you map them here
// If you do not use claims, consider implementing one of the other IUserStore interfaces 
//such as the IUserLoginStore so that you do not have to implement the GetClaimsAsync method
public async Task<IList<Claim>> GetClaimsAsync(MasterUser user, CancellationToken cancellationToken)
{
    var claims = new List<Claim>();

    foreach (var claim in user.Claims)
    {
        claims.Add(new Claim(claim.ClaimType, claim.ClaimValue));
    }

    return claims;
}

These are all the classes you need for custom authentication. No let's configure our custom authentication method in the Startup.cs class. Add the following to the ConfigureServices method

public void ConfigureServices(IServiceCollection services)
{
     // Use the default role, IdentityRole as we are not implementing roles
     // Add our custom UserManager and UserStore classes
     services.AddIdentity<MyLegacyUser, IdentityRole>(config =>
        {
            config.User.RequireUniqueEmail = true;
            config.Cookies.ApplicationCookie.AccessDeniedPath = new Microsoft.AspNet.Http.PathString("/Auth/Login");
            config.Cookies.ApplicationCookie.LoginPath = new Microsoft.AspNet.Http.PathString("/Auth/Login");
            config.Cookies.ApplicationCookie.LogoutPath = new Microsoft.AspNet.Http.PathString("/Auth/Login");
        })
        .AddUserManager<MyLegacyUserManager>()
        .AddUserStore<MyLegacyUserUserClaimStore>()
        .AddEntityFrameworkStores<MyDbContext>();
} 

In the Configure method make sure that you specify that you want to use the Identity functionality to authenticate

Note: The order of your use statements are important, make sure that you include UseIdentity before UseMvc if you are using Mvc.

public async void Configure(IApplicationBuilder app)
{
    app.UseIdentity();
    // Your useMvc and other use statements will go here
}

Now we have configured our custom authentication classes and we can authenticate by using the default SignInManager class. Here is an example of my AuthController class

public class AuthController : Controller
{
    private SignInManager<MyLegacyUserUser> _signInManager;

    public AuthController(SignInManager<MasterUser> signInManager)
    {
        _signInManager = signInManager;
    }

    // For simplicity I will only add the Login action here
    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel loginViewModel)
    {
        var result = await _signInManager.PasswordSignInAsync(loginViewModel.Username, loginViewModel.Password, true, false);

        if (result == SignInResult.Success)
        {
            return RedirectToAction("Index", "SomeControllerToRedirectTo");
        }

        await _signInManager.SignOutAsync();

        return RedirectToAction("Login", "Auth");
    }
}

When your user is authenticated you can access the user claims as you would have done with MVC 5, for example

var email = User.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Email)).Value;

Upvotes: 3

Related Questions