Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customising the OWIN/Katana UserManager factory behaviour

There are many samples online using OWIN/Katana to find users in a database based on ausername/password combination and generate a claims principal, such as...

var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
// generate claims here...

That's fine if you're creating a new application and want Entity Framework to do the dirty work. But, I have an eight year old monolithic web site that has just been updated to use claims-based authentication. Our database hit is done manually via DAL/SQL and then the ClaimsIdentity is generated from there.

Some people are suggesting that OWIN is easier to use than our manual approach, but I'd like some input from those that use it.

Is it possible to alter how the UserManager factory finds users based on their credentials? Or, is there another approach that I've missed? All the samples I can find online seem to use a boilerplate approach of letting Entity Framework create the database and manage the searches.

like image 794
EvilDr Avatar asked Jan 07 '15 17:01

EvilDr


1 Answers

ASP.NET Identity is a little bit overly complex, I would say.
In August 2014 they've announced the new version 2.1 and things have changed again.
First of all let's get rid of EntityFramework:

Uninstall-Package Microsoft.AspNet.Identity.EntityFramework

Now we implement our own definition of User implementing the interface IUser (Microsoft.AspNet.Identity):

public class User: IUser<int>
{
    public User()
    {
        this.Roles = new List<string>();
        this.Claims = new List<UserClaim>();
    }

    public User(string userName)
        : this()
    {
        this.UserName = userName;
    }

    public User(int id, string userName): this()
    {
        this.Id = Id;
        this.UserName = userName;
    }

    public int Id { get; set; }
    public string UserName { get; set; }
    public string PasswordHash { get; set; }

    public bool LockoutEnabled { get; set; }
    public DateTime? LockoutEndDateUtc { get; set; }
    public bool TwoFactorEnabled { get; set; }

    public IList<string> Roles { get; private set; }
    public IList<UserClaim> Claims { get; private set; }
}

As you can see I have defined the type of my Id (int).

Then you have to define your custom UserManager inheriting from Microsoft.AspNet.Identity.UserManager specifying the your user type and the key type.

public class UserManager : UserManager<User, int>
{
    public UserManager(IUserStore<User, int> store): base(store)
    {
        this.UserLockoutEnabledByDefault = false;
        // this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(10);
        // this.MaxFailedAccessAttemptsBeforeLockout = 10;
        this.UserValidator = new UserValidator<User, int>(this)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = false
        };

        // Configure validation logic for passwords
        this.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 4,
            RequireNonLetterOrDigit = false,
            RequireDigit = false,
            RequireLowercase = false,
            RequireUppercase = false,
        };
    }
}

I've implemented my validation rules here but you can keep it outside if you prefer.

UserManager needs a UserStore (IUserStore).

You will define your DB logic here. There are a few interfaces to implement. Not all of them are mandatory though.

public class UserStore : 
    IUserStore<User, int>, 
    IUserPasswordStore<User, int>, 
    IUserLockoutStore<User, int>, 
    IUserTwoFactorStore<User, int>,
    IUserRoleStore<User, int>,
    IUserClaimStore<User, int>
{

    // You can inject connection string or db session
    public UserStore()
    {
    }

}

I haven't included all the methods for each interface. Once you have done that you'll be able to write your new user:

public System.Threading.Tasks.Task CreateAsync(User user)
{
}

fetch it by Id:

public System.Threading.Tasks.Task<User> FindByIdAsync(int userId)
{
}

and so on.

Then you'll need to define your SignInManager inheriting from Microsoft.AspNet.Identity.Owin.SignInManager.

public class SignInManager: SignInManager<User, int>
{
    public SignInManager(UserManager userManager, IAuthenticationManager authenticationManager): base(userManager, authenticationManager)
    {
    }

    public override Task SignInAsync(User user, bool isPersistent, bool rememberBrowser)
    {
        return base.SignInAsync(user, isPersistent, rememberBrowser);
    }
}

I've only implemented SignInAsync: it will generates a ClaimsIdentity.

That's pretty much it.

Now in your Startup class you have to tell Owin how to create the UserManager and the SignInManager.

app.CreatePerOwinContext<Custom.Identity.UserManager>(() => new Custom.Identity.UserManager(new Custom.Identity.UserStore()));
// app.CreatePerOwinContext<Custom.Identity.RoleManager>(() => new Custom.Identity.RoleManager(new Custom.Identity.RoleStore()));
app.CreatePerOwinContext<Custom.Identity.SignInService>((options, context) => new Custom.Identity.SignInService(context.GetUserManager<Custom.Identity.UserManager>(), context.Authentication));

I haven't used the factories you will find in the default template cause I wanted to keep things as simple as possible.

And enable your application to use the cookie:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider
        {
         // Enables the application to validate the security stamp when the user logs in.
         // This is a security feature which is used when you change a password or add an external login to your account.  
         OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<Custom.Identity.UserManager, Custom.Identity.User, int>(
         validateInterval: TimeSpan.FromMinutes(30),
         regenerateIdentityCallback: (manager, user) =>
         {
        var userIdentity = manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
                return (userIdentity);
    },
        getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
        )}
}); 

Now in your account controller - or the controller responsible for the login - you will have to get the UserManager and the SignInManager:

public Custom.Identity.SignInManager SignInManager
{
    get
    {
    return HttpContext.GetOwinContext().Get<Custom.Identity.SignInManager>();
    }
}

public Custom.Identity.UserManager UserManager
{
    get
    {
    return HttpContext.GetOwinContext().GetUserManager<Custom.Identity.UserManager>();
    }
}

You will use the SignInManager for the login:

var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);

and the UserManager to create the user, add roles and claims:

if (ModelState.IsValid)
{
        var user = new Custom.Identity.User() { UserName = model.Email };

        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
    {
        // await UserManager.AddToRoleAsync(user.Id, "Administrators");
                // await UserManager.AddClaimAsync(user.Id, new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Country, "England"));

                await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);

        return RedirectToAction("Index", "Home");
    }
        AddErrors(result);
}

It seems complicate ... and it is ... kind of.

If you want to read more about it there's a good explanation here and here.

If you want to run some code and see how it works, I've put together some code which works with Biggy (as I didn't want to waste to much time defining tables and stuff like that).

If you have the chance to download my code from the github repo, you'll notice that I have created a secondary project (Custom.Identity) where I've kept all my ASP.NET Identity stuff.

The only nuget packages you will need there are:

  1. Microsoft.AspNet.Identity.Core
  2. Microsoft.AspNet.Identity.Owin
like image 60
LeftyX Avatar answered Oct 13 '22 01:10

LeftyX