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?
SignInAsync(HttpContext, String, ClaimsPrincipal, AuthenticationProperties) Sign in a principal for the specified scheme.
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.
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.
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.
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”);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With