Reputation: 7105
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
Upvotes: 2
Views: 639
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