5

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.

LeftyX
  • 35,328
  • 21
  • 132
  • 193
EvilDr
  • 8,943
  • 14
  • 73
  • 133
  • Yes you can customize how the UserManager behaves. The way it gets its data is through the implementation of the `IUserStore`. Here's an answer that may help you out on how to customize this. http://stackoverflow.com/questions/19940014/asp-net-identity-with-ef-database-first-mvc5/21122865#21122865 – jamesSampica Jan 08 '15 at 17:06

1 Answers1

13

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
Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101
LeftyX
  • 35,328
  • 21
  • 132
  • 193
  • Amazing answer - thank you very much. SO much code though for something relatively simple. What is the driver for all this? Is it to allow the app to not be aware of *where* the data actually sits, e.g. better interoperability? – EvilDr Jan 12 '15 at 11:48
  • 1
    Agree with you. I am wondering if we really need to go through all these steps to implement something which should be straightforward. The driver is to have your own tables/schema for security. I - personally - don't like entity framework and want to get rid of it. Extensibility maybe. At some point you might decide to switch to some NoSql databases. – LeftyX Jan 12 '15 at 11:58
  • 1
    Agree about Entity Framework. Out of interest, how did you get so familiar with the above code? Did someone point you in the right direction and you picked it up from experience? I find I don't have enough time in the day to keep up with ASP.NET's rapid changes any more... – EvilDr Jan 12 '15 at 12:08
  • 1
    I've been using Owin with web.api for quite some time now. I was working on a project which was a hybrid mobile app with bearer tokens provided by the web.api. I liked the concept so I wanted to replicate the logic in a new MVC5 project I am working on at the moment. Since I use nHibernate, I wanted to get rid of EF and replace it with my own implementation with nHibernate. Another reason is the default MVC5 template does not use Forms Authentication any more. I went thorough loads of blogs and papers and worked on sample solution. The one you can find on github. – LeftyX Jan 12 '15 at 12:17
  • 1
    Yes, I cannot agree more. At the moment I am even considering to drop all that stuff and go back to my regular authentication/authorization pipeline. Microsoft is well-know to change things or - even worse - drop things every two years. – LeftyX Jan 12 '15 at 12:19
  • I've just replaced Forms Auth in my web site with the Identity/Claims approach, so I feel your pain. Given the opinions for/against EF and OWIN, do you think its likely to change massively in the future? I don't want to spend time on it if I then have to start over because MS decide it could have been better... – EvilDr Jan 12 '15 at 12:19
  • 1
    It will change. That's for sure. My perception is they're playing with it. Look at all those interfaces. Do you really need that level of abstraction? – LeftyX Jan 12 '15 at 12:22
  • At the risk of being shot down for not moving this to a chat thread, thanks very much, and if you fancy a new job on David's side fighting industry Goliaths, let me know :-) – EvilDr Jan 12 '15 at 12:36
  • You're welcome. I am always interested in new projects :-) If you're happy with my answer don't forget to accept it. – LeftyX Jan 12 '15 at 14:56