ASP.NET Core Custom User Manager

ASP.NET Core Custom User Manager
ASP.NET Core Custom User Manager

In some purpose you may prefer to keep away from the use of the general Identity package deal to work with users, roles, permissions etc. I had two reasons:

That’s why I determined to write my own. But I had to exchange solely user/role/permission management, whilst the fashionable ASP.NET Core signal in/sign out points had to be nevertheless in use.

We will write easy net utility that will have its very own consumer supervisor to validate, signal in (using the preferred ASP.NET Core implementation), and signal out users. It will use SQLite database to save associated facts and Entity Framework as the ORM, however it is effortless to exchange it with any different storage and ORM you want.

Architecture

I favor our person supervisor to help one of a kind login sorts (email and password, Microsoft, Google, Facebook etc.) and I prefer it to be effortless to add new ones. Also, I would like User object to have data solely about the user, now not about the approaches he logs in. I don’t like the way it is carried out in Identity:

public class IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin> where TKey : IEquatable<TKey>
{
  public IdentityUser();
  public IdentityUser(string userName);
  public virtual ICollection<TUserRole> Roles { get; }
  public virtual int AccessFailedCount { get; set; }
  public virtual bool LockoutEnabled { get; set; }
  public virtual DateTimeOffset? LockoutEnd { get; set; }
  public virtual bool TwoFactorEnabled { get; set; }
  public virtual bool PhoneNumberConfirmed { get; set; }
  public virtual string PhoneNumber { get; set; }
  public virtual string ConcurrencyStamp { get; set; }
  public virtual string SecurityStamp { get; set; }
  public virtual string PasswordHash { get; set; }
  public virtual bool EmailConfirmed { get; set; }
  public virtual string NormalizedEmail { get; set; }
  public virtual string Email { get; set; }
  public virtual string NormalizedUserName { get; set; }
  public virtual string UserName { get; set; }
  public virtual TKey Id { get; set; }
  public virtual ICollection<TUserClaim> Claims { get; }
  public virtual ICollection<TUserLogin> Logins { get; }
  public override string ToString();
}

Why we have so large class? What if we use solely Facebook login? In this case most of these houses (PhoneNumber, PhoneNumberConfirmed, Email, EmailConfirmed, PasswordHash etc.) are useless. Of course, that is now not so huge deal, however it is no longer stylish from my factor of view.
I determined to have three fundamental classes: User, Credential, and CredentialType. Each login kind supported through the software is described by using the corresponding CredentialType type object:

public class CredentialType
{
  public int Id { get; set; }
  public string Code { get; set; }
  public string Name { get; set; }
  public int? Position { get; set; }

  public virtual ICollection<Credential> Credentials { get; set; }
}

Login statistics of the unique consumer is described via the set of the Credential classification objects (one per credential type):

public class Credential
{
  public int Id { get; set; }
  public int UserId { get; set; }
  public int CredentialTypeId { get; set; }
  public string Identifier { get; set; }
  public string Secret { get; set; }

  public virtual User User { get; set; }
  public virtual CredentialType CredentialType { get; set; }
}

As you can see, there are the Identifier and Secret houses in this class. In context of electronic mail and password authentication, they incorporate e-mail and password hash respectively. In case of Facebook authentication, Identifier property will comprise Facebook ID, whilst Secret property cost will be null.
User is represented by way of the User type and incorporates nothing barring user-related information:

public class User
{
  public int Id { get; set; }
  public string Name { get; set; }
  public DateTime Created { get; set; }

  public virtual ICollection<Credential> Credentials { get; set; }
}

I suppose this separation is useful, due to the fact person is person and the way how he logs in into the software is some thing different.
Also, we have 4 greater classes: Role, UserRole, Permission, and UserPermission. I will no longer pay too a good deal interest to these four to hold publish simple, however you can seem to be at them in the pattern net utility (see hyperlink at the bottom). The fundamental thought is that roles can be assigned to the user, and permissions can be assigned to the role. We should additionally make it feasible to assign permissions to the consumer immediately too, however it is now not so essential now.

User Manager Implementation

First of all, we have to flip on cookies authentication in our net application. This is carried out my registering corresponding middleware inner the Configure approach of the Startup class:

applicationBuilder.UseCookieAuthentication(
  new CookieAuthenticationOptions()
  {
    AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    CookieName = "AspNetCoreCustomUserManager",
    ExpireTimeSpan = TimeSpan.FromMinutes(10)
  }
);

Now let’s create the UserManager class. The most vital technique of this category is the Validate one:

public User Validate(string loginTypeCode, string identifier, string secret)
{
  CredentialType credentialType = this.storage.CredentialTypes.FirstOrDefault(ct => string.Equals(ct.Code, loginTypeCode, StringComparison.OrdinalIgnoreCase));

  if (credentialType == null)
    return null;

  Credential credential = this.storage.Credentials.FirstOrDefault(
    c => c.CredentialTypeId == credentialType.Id && string.Equals(c.Identifier, identifier, StringComparison.OrdinalIgnoreCase) && c.Secret == MD5Hasher.ComputeHash(secret)
  );

  if (credential == null)
    return null;

  return this.storage.Users.Find(credential.UserId);
}

It tries to discover consumer with given identifier and password inside given credential type. PBKDF2 transformation is utilized to the secret automatically. If matching credential is found, corresponding person is returned.
Next technique logs in the user:

public async Task SignIn(HttpContext httpContext, User user, bool isPersistent = false)
{
  ClaimsIdentity identity = new ClaimsIdentity(this.GetUserClaims(user), CookieAuthenticationDefaults.AuthenticationScheme);
  ClaimsPrincipal principal = new ClaimsPrincipal(identity);

  await httpContext.Authentication.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties() { IsPersistent = isPersistent }
  );
}

The key aspect right here is calling of the GetUserClaims method. It receives all of the person roles and permissions and creates corresponding claims for them. Also, it creates claims for person ID and name:

private IEnumerable<Claim> GetUserClaims(User user)
{
  List<Claim> claims = new List<Claim>();

  claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
  claims.Add(new Claim(ClaimTypes.Name, user.Name));
  claims.AddRange(this.GetUserRoleClaims(user));
  return claims;
}

This approves developer to get all the consumer claims when person is logged in and take a look at them and to prevent get admission to to unique assets primarily based on this information.

Using in a Web Application

We will check how our customized person supervisor works the usage of extraordinarily easy net application. First of all, register our UserManager classification interior the DI (using scoped lifetime is important, due to the fact UserManager makes use of Storage service, which is registered as scoped; we will now not pay interest on the Storage service, however it’s simply the Entity Framework database context):

services.AddScoped<IUserManager, UserManager>();

Now, create the HomeController type with the few methods:

public class HomeController : Controller
{
  private IUserManager userManager;

  public HomeController(IUserManager userManager)
  {
    this.userManager = userManager;
  }

  [HttpGet]
  public IActionResult Index()
  {
    return this.View();
  }

  [HttpPost]
  public IActionResult Login()
  {
    User user = this.userManager.Validate("Email", "[email protected]", "admin");

    if (user != null)
      this.userManager.SignIn(this.HttpContext, user, false);

    return this.RedirectToAction("Index");
  }

  [HttpPost]
  public IActionResult Logout()
  {
    this.userManager.SignOut(this.HttpContext);
    return this.RedirectToAction("Index");
  }
}

Our view will show the facts about logged in consumer (if consumer is authenticated), and Login/Logout consumer buttons. These buttons (inside the corresponding forms) will make easy POST requests to our controller’s Login and Logout methods, which will then redirect you to the index web page again. To make the whole lot as easy as possible, user’s login and password are hardcoded internal the Login method, however you can exchange this with login web page instead.


Here is our view:

@inject AspNetCoreCustomUserManager.IUserManager UserManager
@using System.Security.Claims
<h1>ASP.NET Core<br />Custom User Manager</h1>
<div class="form__field field">
  <span class="marker marker--secondary">User.Identity.IsAuthenticated:</span> @User.Identity.IsAuthenticated
</div>
@if (this.User.Identity.IsAuthenticated)
{
  <div class="form__field field">
    <span class="marker marker--secondary">UserManager.GetCurrentUser(this.Context).Name:</span> @UserManager.GetCurrentUser(this.Context).Name
  </div>
  <div class="form__field field">
    <span class="marker marker--secondary">User.HasClaim(ClaimTypes.Role, "Administrator"):</span> @User.HasClaim(ClaimTypes.Role, "Administrator")
  </div>
  <div class="form__field field">
    <span class="marker marker--secondary">User.HasClaim("Permission", "DoEverything"):</span> @User.HasClaim("Permission", "DoEverything")
  </div>
}
@if (this.User.Identity.IsAuthenticated)
{
  <form class="form" action="/home/logout" method="post">
    <div class="form__buttons buttons">
      <button type="submit">Logout user</button>
    </div>
  </form>
}
else
{
  <form class="form" action="/home/login" method="post">
    <div class="form__buttons buttons">
      <button type="submit">Login user</button>
    </div>
  </form>
}

You can see that we use trendy ASP.NET Core User.Identity.IsAuthenticated property to test whether or not the consumer is authenticated or not. Also, we use equal User property to take a look at claims. While our UserManager is registered internal the DI, we can inject it into the view and use to show present day user’s identify as well.

Conclusions

I have created the demo assignment for this post. Feel free to ask if you have any questions!

45 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

fifteen − 12 =

You May Also Like