33

I have a MVC 6 project (vNext) and I am playing around with the ASP.NET Identity. In my case I don't want to use the build-in stuff which uses the EF (SignInManager, UserManager, UserStore). I have an external database and I just want to make a username/password lookup and return a valid cookie. So I started writing my own classes.

public class MyUser
{
    public string Id { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public string PasswordHash { get; set; }
}

public class MyUserStore : IUserStore<MyUser>, IUserPasswordStore<MyUser>
{
    ...
}

In the MyUserStore class I am using hard-coded list of users as my store (only for test purposes). And I overrode some methods just to return the data from the hard-coded store.

public class MyUserManager : UserManager<MyUser>
{
    public MyUserManager(
        IUserStore<MyUser> store,
        IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<MyUser> passwordHasher,
        IEnumerable<IUserValidator<MyUser>> userValidators,
        IEnumerable<IPasswordValidator<MyUser>> passwordValidators,
        ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors,
        IEnumerable<IUserTokenProvider<MyUser>> tokenProviders,
        ILoggerFactory logger,
        IHttpContextAccessor contextAccessor) :
        base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, tokenProviders, logger, contextAccessor)
    {
    }
}

Here I made the methods CheckPasswordAsync and VerifyPasswordAsync to return true and PasswordVerificationResult.Success respectively just for the test.

public class MyClaimsPrincipleFactory : IUserClaimsPrincipalFactory<MyUser>
{
    public Task<ClaimsPrincipal> CreateAsync(MyUser user)
    {
        return Task.Factory.StartNew(() =>
        {
            var identity = new ClaimsIdentity();
            identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
            var principle = new ClaimsPrincipal(identity);

            return principle;
        });
    }
}

public class MySignInManager : SignInManager<MyUser>
{
    public MySignInManager(MyUserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<MyUser> claimsFactory, IOptions<IdentityOptions> optionsAccessor = null, ILoggerFactory logger = null)
            : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger)
    {
    }

    public override Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
    {
        // here goes the external username and password look up

        if (userName.ToLower() == "username" && password.ToLower() == "password")
        {
            return base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);
        }
        else
        {
            return Task.FromResult(SignInResult.Failed);
        }
    }
}

And everything is hooked up in the Startup class as follows:

services.AddIdentity<MyUser, MyRole>()
            .AddUserStore<MyUserStore>()
            .AddUserManager<MyUserManager>()
            .AddDefaultTokenProviders();

And because I didn't manage to create the MySignInManager object in the Startup code in order to add it into the DI (for later injection in the controllers and views), I am creating it in the MyAccountController.

public MyAccountController(IHttpContextAccessor httpContextAccessor, UserManager<MyUser> userManager, IOptions<IdentityOptions> optionsAccessor, ILoggerFactory logger)
{
    SignInManager = new MySignInManager(userManager as MyUserManager, httpContextAccessor, new MyClaimsPrincipleFactory(), optionsAccessor, logger);
}

In my MyLogin action in the MyAccount controller I am calling PasswordSignInAsync and I can see that I am getting the cookie with the encoded claims in it (from the MyClaimsPrincipleFactory). When I try to call some other action with the AuthorizeAttribute on it I can see that the cookie is in the request header but I am unauthorized (more precisely, because I didn't remove the built-in default ASP.NET Identity Authentication from the visual studio sample template, I am redirected to the Account/Login instead).

Is this the right way of customizing ASP.NET Identity and what am I missing here?

Draken
  • 3,134
  • 13
  • 34
  • 54
Dilyan Dimitrov
  • 789
  • 1
  • 7
  • 24
  • Put a breakpoint on this line return base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout); and see what it returns. My guess is that it too is returning SignInResult.Failed. I would return Task.FromResult(SignInResult.Success) instead of calling the base class... hth –  Jul 07 '15 at 14:40
  • Yeah I tried that one. When I am returning Success without calling the base method, I am not getting the cookie. – Dilyan Dimitrov Jul 07 '15 at 15:58

4 Answers4

12

I had problems too trying to use a custom SignInManager and turns out to be really easy after all to implement.

In Startup.cs, after the default implementation of services.Identity

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

You only need to inject into the built-in DI the following:

services.AddScoped<SignInManager<MyApplicationUser>, MySignInManager>();

The default SignInManager is overwrited by the custom one.

SpruceMoose
  • 9,737
  • 4
  • 39
  • 53
MissRaphie
  • 595
  • 1
  • 9
  • 27
7

In my project I have a working implementation of identity without using EF. I think maybe you are implementing more than you need to. UserManager and SignInManager are not tied to EF. You can implement those if you want but you don't have to just to get away from EF. you only really need to implement the UserStore and RoleStore and maybe PasswordHasher if you are needing to validate hard coded password for testing.

services.TryAdd(ServiceDescriptor.Scoped<IUserStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserPasswordStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserEmailStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserLoginStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserRoleStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserClaimStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserPhoneNumberStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserLockoutStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserTwoFactorStore<SiteUser>, UserStore<SiteUser>>());
services.TryAdd(ServiceDescriptor.Scoped<IRoleStore<SiteRole>, RoleStore<SiteRole>>());
services.TryAdd(ServiceDescriptor.Scoped<IUserClaimsPrincipalFactory<SiteUser>, SiteUserClaimsPrincipalFactory<SiteUser, SiteRole>>());
services.TryAdd(ServiceDescriptor.Transient<IPasswordHasher<SiteUser>, SitePasswordHasher<SiteUser>>());
services.AddIdentity<SiteUser, SiteRole>();

the above shows the items I am implementing to bypass Entity Framework, my AccountController for example takes constructor parameters for

    UserManager<SiteUser> userManager,
            SignInManager<SiteUser> signInManager

which are the standard identity UserManager and SignInManager that I did not have to setup with DI services they are registered for me by this line:

services.AddIdentity<SiteUser, SiteRole>();

you can see the code for that extension method here. It is part of Identity not part of EFIdentity. You can see that I also implemented IUserClaimsPrincipalFactory. The only reason I implemented that was to add some custom claims, I did not need to do that to get away from EF.

Joe Audette
  • 35,330
  • 11
  • 106
  • 99
  • I may not really have needed to implement IRoleStore since it isn't actually used anywhere in the starter web app code. My "implementation" just throws NotImplementedException in every method but I never encounter the exception because it is not used anywhere. Presumably in future versions of the VS web app template there will be controllers/views to manage roles and it will be needed then but not now. – Joe Audette Jul 07 '15 at 16:12
  • Okey, I removed my custom `SignInManager` and `UserManager`. Still when I try to go to the action with `Authorize` attribute I am getting 302 and it redirects me to the /Account/Login - the default one. But I have MyAccountController working with my custom IUserStore, MyUser, etc.... – Dilyan Dimitrov Jul 08 '15 at 07:32
  • I realized that after I login I have my `ClaimsIdentity` in the `User.Identity` object. But the `IsAuthenticated` property is `false`. In the fiddler I can see that the cookie is there and it is passed with each request. – Dilyan Dimitrov Jul 08 '15 at 08:05
  • 2
    you do still have this line in Startup.cs right? app.UseIdentity(); that is the one that sets up cookie auth – Joe Audette Jul 08 '15 at 13:16
5

You can register a custom SignInManager for Dependency Injection by using the IdentityBuilder.AddSignInManager method within your Startup.ConfigureServices method as follows:

services.AddIdentity<MyUser, IdentityRole<int>>()
    .AddUserStore<UserStore<MyUser, IdentityRole<int>, SqlContext, int>>()
    .AddRoleStore<RoleStore<IdentityRole<int>, SqlContext, int>>()
    .AddSignInManager<SignInManager<MyUser>>()
    .AddDefaultTokenProviders();

There is no reason that the SignInManager you have implemented cannot be registered for DI in the same way.

SpruceMoose
  • 9,737
  • 4
  • 39
  • 53
4

The issue here was that I didn't provide the AuthenticationType of my ClaimsIdentity object. This blog post helped me.

To have IsAuthenticated set to true, you need to specify an authentication type in the ctor:

var id = new ClaimsIdentity(claims, “Custom”);

Community
  • 1
  • 1
Dilyan Dimitrov
  • 789
  • 1
  • 7
  • 24