Skip to content
This repository has been archived by the owner on Dec 20, 2018. It is now read-only.

Custom IdentityUserLogin<int> and EF user store #1878

Closed
dazinator opened this issue Jul 15, 2018 · 3 comments
Closed

Custom IdentityUserLogin<int> and EF user store #1878

dazinator opened this issue Jul 15, 2018 · 3 comments

Comments

@dazinator
Copy link

dazinator commented Jul 15, 2018

I'm using the EF stores.

The only change I'd like to make is to add a column to the AspNetUserLogins table so that I can also store the refresh token associated with the external login. I think adding a column to an EF model should be fairly straightforward, and it was, but then getting identity to work with it is turning out to be a bit painful :-)

I derived my own entity from IdentityUserLogin<int> and added the additional property:

   public class DennisUserLogin : IdentityUserLogin<int>
   {
        public string RefreshToken { get; set; }
   }

And therefore had to derive my own IdentityDbContext, passing in that replacement type:


 public class DennisContext : IdentityDbContext<DennisUser, IdentityRole<int>, int, IdentityUserClaim<int>, IdentityUserRole<int>, DennisUserLogin, IdentityRoleClaim<int>, IdentityUserToken<int>>
    {
        public DennisContext(DbContextOptions<DennisContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);         
            
        }
    }

I was able to then add a new ef migration, and apply that, and I can see the additional column in the database - great.

Next I needed to be able to set this new property. So I had to find the locations where a new IdentityUserLogin entity is created, so that I could also set my additional property before its saved.

Unfortunately the UserStore seems to create this entity, and the there is nowhere in the existing API where I could pass an additional value to it (i.e referesh token). Therefore overriding any existing method on UserStore or UserManager wouldn't cater for my scenario. So I had to add some additional method to UserManager and UserStore that also took a "refresh token":


  public class DennisUserStore : UserStore<DennisUser, IdentityRole<int>, DennisContext, int>, IDennisUserStore
    {

        private readonly DennisContext _context;

        public DennisUserStore(DennisContext context, IdentityErrorDescriber describer = null) : base(context, describer)
        {
            _context = context;
        }
        
        
        /// <summary>
        /// Adds the <paramref name="login"/> given to the specified <paramref name="user"/>.
        /// </summary>
        /// <param name="user">The user to add the login to.</param>
        /// <param name="login">The login to add to the user.</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        public Task AddLoginAsync(DennisUser user, UserLoginInfo login, string refreshToken,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }
            if (login == null)
            {
                throw new ArgumentNullException(nameof(login));
            }
            var userLogins = _context.UserLogins;
            var newLogin = CreateDennisUserLogin(user, login);
            newLogin.RefreshToken = refreshToken;
            userLogins.Add(newLogin);
            return Task.FromResult(false);
        }

        private DennisUserLogin CreateDennisUserLogin(DennisUser user, UserLoginInfo login)
        {
            var dennisUserLogin = new DennisUserLogin();
            dennisUserLogin.LoginProvider = login.LoginProvider;
            dennisUserLogin.ProviderDisplayName = login.ProviderDisplayName;
            dennisUserLogin.ProviderKey = login.ProviderKey;
            dennisUserLogin.UserId = user.Id;
            return dennisUserLogin;
        }

    }

and UserManager:



   public class DennisUserManager : UserManager<DennisUser>
    {
        public DennisUserManager(IUserStore<DennisUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<DennisUser> passwordHasher, IEnumerable<IUserValidator<DennisUser>> userValidators, IEnumerable<IPasswordValidator<DennisUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<DennisUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
        {

        }

        // IUserLoginStore methods
        private IDennisUserStore GetStore()
        {

            var cast = Store as IDennisUserStore;
            if (cast == null)
            {
                throw new NotSupportedException("Store Not IUserLoginStore");
            }
            return cast;
        }     

        public async Task<IdentityResult> AddLoginAsync(DennisUser user, UserLoginInfo login, string refreshToken)
        {
            ThrowIfDisposed();
            var loginStore = GetStore();
            if (login == null)
            {
                throw new ArgumentNullException(nameof(login));
            }
            if (user == null)
            {
                throw new ArgumentNullException(nameof(user));
            }

            var existingUser = await FindByLoginAsync(login.LoginProvider, login.ProviderKey);
            if (existingUser != null)
            {
                Logger.LogWarning(4, "AddLogin for user {userId} failed because it was already associated with another user.", await GetUserIdAsync(user));
                return IdentityResult.Failed(ErrorDescriber.LoginAlreadyAssociated());
            }
            await loginStore.AddLoginAsync(user, login, refreshToken, CancellationToken);
            return await UpdateUserAsync(user);
        }
    }

So far this is a lot of work in order to be able to add one property to this entity but I thought I was close.

I changed the razor UI code to call the new method on my UserManager instead which takes the refresh token,

Now when I run the app, when attempting an external login, I get the following error:

InvalidOperationException: Cannot create a DbSet for 'IdentityUserLogin' because this type is not included in the model for the context.
Microsoft.EntityFrameworkCore.Internal.InternalDbSet.get_EntityType()
Microsoft.EntityFrameworkCore.Internal.InternalDbSet.get_EntityQueryable()
Microsoft.EntityFrameworkCore.Internal.InternalDbSet.System.Linq.IQueryable.get_Provider()
Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync<TSource, TResult>(MethodInfo operatorMethodInfo, IQueryable source, Expression expression, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync<TSource, TResult>(MethodInfo operatorMethodInfo, IQueryable source, LambdaExpression expression, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(IQueryable source, Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken)
Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>.FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>.FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
Microsoft.AspNetCore.Identity.SignInManager.ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
Dennis.Areas.Identity.Pages.Account.ExternalLoginModel.OnGetCallbackAsync(string returnUrl, string remoteError) in ExternalLogin.cshtml.cs

  •       {
              ErrorMessage = "Error loading external login information.";
              return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
          }
          // Sign in the user with this external login provider if the user already has a login.
          var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true);
          if (result.Succeeded)
          {
              var tokens = info.AuthenticationTokens.ToArray();
              // save the users 
              _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
              return LocalRedirect(returnUrl);
    

Microsoft.AspNetCore.Mvc.RazorPages.Internal.ExecutorFactory+GenericTaskHandlerMethod.Convert(object taskAsObject)

It seems something about the store is still specifically looking for IdentityUserLogin<int> which isn't part of my model anymore - because I am using my own derived type DennisUserLogin. However it doesn't appear I can inform the user store of that.. What am I missing?

@dazinator dazinator changed the title Customise UserLogin ef entity Custom IdentityUserLogin<int> and EF user store Jul 15, 2018
@dazinator
Copy link
Author

dazinator commented Jul 15, 2018

Ah damn, I missed the overload of UserStore that takes the type I needed.

I should have declared it like this:

 public class DennisUserStore : UserStore<DennisUser, IdentityRole<int>, DennisContext, int, IdentityUserClaim<int>, IdentityUserRole<int>, DennisUserLogin, IdentityUserToken<int>, IdentityRoleClaim<int>>, IDennisUserStore
    {

@ajcvickers
Copy link
Member

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants