Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET 5 Identity - custom SignInManager

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?

like image 741
Dilyan Dimitrov Avatar asked Jul 07 '15 14:07

Dilyan Dimitrov


People also ask

What does HttpContext SignInAsync do?

SignInAsync(HttpContext, String, ClaimsPrincipal, AuthenticationProperties) Sign in a principal for the specified scheme.


4 Answers

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.

like image 183
MissRaphie Avatar answered Sep 29 '22 22:09

MissRaphie


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.

like image 39
Joe Audette Avatar answered Sep 30 '22 22:09

Joe Audette


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.

like image 24
SpruceMoose Avatar answered Oct 03 '22 22:10

SpruceMoose


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”);

like image 28
Dilyan Dimitrov Avatar answered Sep 30 '22 22:09

Dilyan Dimitrov