2017-08-25 11 views
2

J'ai quelques applications Web ASP.NET héritées qui partagent une base de données pour l'adhésion ASP.NET. Je souhaite passer à une architecture de microservices utilisant .NET Core et IdentityServer4 et avoir le serveur d'identité dans le nouvel écosystème microservices pour utiliser le magasin d'utilisateurs ASP.NET Membership existant, mais .NET Core ne semble pas prendre en charge l'adhésion ASP.NET à tout.Comment consommer et base de données d'adhésion ASP.NET dans l'identité ASP.NET?

J'ai actuellement une preuve de concept qui a impliqué une API Web, un serveur d'identité et une application Web MVC en tant que client. Le serveur d'identité implémente une sous-classe de IdentityUser et implémente IUserStore/IUserPasswordStore/IUserEmailStore pour l'adapter aux tables d'appartenance ASP.NET dans ma base de données existante. Je peux enregistrer de nouveaux utilisateurs et me connecter via mon application client POC MVC, mais ces utilisateurs ne peuvent pas se connecter à mes anciennes applications. À l'inverse, les utilisateurs enregistrés dans les anciennes applications ne peuvent pas se connecter à mon client POC MVC. Je suppose que c'est parce que mon implémentation de IPasswordHasher n'est pas de hacher les mots de passe de la même manière que l'adhésion ASP.NET dans mes anciennes applications.

Voici mon code. Toute idée de ce que je pourrais faire de mal serait grandement appréciée. La sécurité et la cryptographie ne sont pas mon fort.

Startup.cs

public class Startup 
{ 
    public Startup(IHostingEnvironment env) 
    { 
     var builder = new ConfigurationBuilder() 
      .SetBasePath(env.ContentRootPath) 
      .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); 

     if (env.IsDevelopment()) 
     { 
      // For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709 
      builder.AddUserSecrets<Startup>(); 
     } 

     builder.AddEnvironmentVariables(); 
     Configuration = builder.Build(); 
    } 

    public IConfigurationRoot Configuration { get; } 

    // This method gets called by the runtime. Use this method to add services to the container. 
    public void ConfigureServices(IServiceCollection services) 
    { 
     /* Add CORS policy */ 
     services.AddCors(options => 
     { 
      // this defines a CORS policy called "default" 
      options.AddPolicy("default", policy => 
      { 
       policy.WithOrigins("http://localhost:5003") 
        .AllowAnyHeader() 
        .AllowAnyMethod(); 
      }); 
     }); 
     services.AddMvcCore() 
      .AddAuthorization() 
      .AddJsonFormatters(); 

     /* Add MVC componenets. */ 
     services.AddMvc(); 

     /* Configure IdentityServer. */ 
     services.Configure<IdentityOptions>(options => 
     { 
      // Password settings 
      options.Password.RequireDigit = true; 
      options.Password.RequiredLength = 8; 
      options.Password.RequireNonAlphanumeric = false; 
      options.Password.RequireUppercase = true; 
      options.Password.RequireLowercase = false; 

      // Lockout settings 
      options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); 
      options.Lockout.MaxFailedAccessAttempts = 10; 

      // Cookie settings 
      options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150); 
      options.Cookies.ApplicationCookie.LoginPath = "/Account/Login"; 
      options.Cookies.ApplicationCookie.LogoutPath = "/Account/Logout"; 

      // User settings 
      options.User.RequireUniqueEmail = true; 
     }); 

     /* Add the DbContext */ 
     services.AddDbContext<StoreContext>(options => 
      options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString"))); 

     /* Add ASP.NET Identity to use for registration and authentication. */ 
     services.AddIdentity<AspNetMembershipUser, IdentityRole>() 
      .AddEntityFrameworkStores<StoreContext>() 
      .AddUserStore<AspNetMembershipUserStore>() 
      .AddDefaultTokenProviders(); 

     services.AddTransient<IPasswordHasher<AspNetMembershipUser>, AspNetMembershipPasswordHasher>(); 

     /* Add IdentityServer and its components. */ 
     services.AddIdentityServer() 
      .AddInMemoryCaching() 
      .AddTemporarySigningCredential() 
      .AddInMemoryApiResources(Config.GetApiResources()) 
      .AddInMemoryIdentityResources(Config.GetIdentityResources()) 
      .AddInMemoryClients(Config.GetClients()); 
    } 

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 
    { 
     /* Configure logging. */ 
     loggerFactory.AddConsole(Configuration.GetSection("Logging")); 

     if (env.IsDevelopment()) 
     { 
      loggerFactory.AddDebug(); 
      app.UseDeveloperExceptionPage(); 
      app.UseDatabaseErrorPage(); 
      app.UseBrowserLink(); 
     } 
     else 
     { 
      app.UseExceptionHandler("/Home/Error"); 
     } 

     /* Configure wwwroot */ 
     app.UseStaticFiles(); 

     /* Configure CORS */ 
     app.UseCors("default"); 

     /* Configure AspNet Identity */ 
     app.UseIdentity(); 

     /* Configure IdentityServer */ 
     app.UseIdentityServer(); 

     /* Configure MVC */ 
     app.UseMvc(routes => 
     { 
      routes.MapRoute(
       name: "default", 
       template: "{controller=Home}/{action=Index}/{id?}"); 
     }); 
    } 
} 

AspNetMembershipUser.cs

public class AspNetMembershipUser : IdentityUser 
{ 
    public string PasswordSalt { get; set; } 
    public int PasswordFormat { get; set; } 
} 

AspNetMembershipUserStore.cs

public class AspNetMembershipUserStore : IUserStore<AspNetMembershipUser>, IUserPasswordStore<AspNetMembershipUser>, IUserEmailStore<AspNetMembershipUser> 
{ 
    private readonly StoreContext _dbcontext; 

    public AspNetMembershipUserStore(StoreContext dbContext) 
    { 
     _dbcontext = dbContext; 
    } 

    public Task<IdentityResult> CreateAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => 
     { 
      try 
      { 
       User dbUser = new User(); 
       this.Convert(user, dbUser); 
       _dbcontext.Users.Add(dbUser); 
       _dbcontext.SaveChanges(); 
       return IdentityResult.Success; 
      } 
      catch (Exception ex) 
      { 
       return IdentityResult.Failed(new IdentityError 
       { 
        Code = ex.GetType().Name, 
        Description = ex.Message 
       }); 
      } 
     }); 
    } 

    public Task<IdentityResult> DeleteAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => 
     { 
      try 
      { 
       User dbUser = _dbcontext.Users 
        .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) 
        .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) 
        .Include(u => u.UserGroups) 
        .SingleOrDefault(u => u.ProviderUserName == user.NormalizedUserName); 

       if (dbUser != null) 
       { 
        _dbcontext.AspNetUsers.Remove(dbUser.AspNetUser); 
        _dbcontext.Users.Remove(dbUser); 
        _dbcontext.SaveChanges(); 
       } 

       return IdentityResult.Success; 
      } 
      catch (Exception ex) 
      { 
       return IdentityResult.Failed(new IdentityError 
       { 
        Code = ex.GetType().Name, 
        Description = ex.Message 
       }); 
      } 
     }); 
    } 

    public void Dispose() 
    { 
     _dbcontext.Dispose(); 
    } 

    public Task<AspNetMembershipUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => 
     { 
      User dbUser = _dbcontext.Users 
       .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) 
       .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) 
       .Include(u => u.UserGroups) 
       .SingleOrDefault(u => u.ProviderEmailAddress == normalizedEmail); 

      if (dbUser == null) 
      { 
       return null; 
      } 

      AspNetMembershipUser user = new AspNetMembershipUser(); 
      this.Convert(dbUser, user); 
      return user; 
     }); 
    } 

    public Task<AspNetMembershipUser> FindByIdAsync(string userId, CancellationToken cancellationToken) 
    { 
     long lUserId = long.Parse(userId); 
     return Task.Factory.StartNew(() => 
     { 
      User dbUser = _dbcontext.Users 
       .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) 
       .Include(u => u.AspNetUsers).ThenInclude(u=> u.AspNetApplication) 
       .Include(u => u.UserGroups) 
       .SingleOrDefault(u => u.UserId == lUserId); 

      if (dbUser == null) 
      { 
       return null; 
      } 

      AspNetMembershipUser user = new AspNetMembershipUser(); 
      this.Convert(dbUser, user); 
      return user; 
     }); 
    } 

    public Task<AspNetMembershipUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => 
     { 
      User dbUser = _dbcontext.Users 
       .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) 
       .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) 
       .Include(u => u.UserGroups) 
       .SingleOrDefault(u => u.ProviderUserName == normalizedUserName); 

      if (dbUser == null) 
      { 
       return null; 
      } 

      AspNetMembershipUser user = new AspNetMembershipUser(); 
      this.Convert(dbUser, user); 
      return user; 
     }); 
    } 

    public Task<string> GetEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.Email); 
    } 

    public Task<bool> GetEmailConfirmedAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.EmailConfirmed); 
    } 

    public Task<string> GetNormalizedEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.NormalizedEmail); 
    } 

    public Task<string> GetNormalizedUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.NormalizedUserName); 
    } 

    public Task<string> GetPasswordHashAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.PasswordHash); 
    } 

    public Task<string> GetUserIdAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.Id.ToString()); 
    } 

    public Task<string> GetUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.UserName); 
    } 

    public Task<bool> HasPasswordAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => !string.IsNullOrEmpty(user.PasswordHash)); 
    } 

    public Task SetEmailAsync(AspNetMembershipUser user, string email, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.Email = email); 
    } 

    public Task SetEmailConfirmedAsync(AspNetMembershipUser user, bool confirmed, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.EmailConfirmed = confirmed); 
    } 

    public Task SetNormalizedEmailAsync(AspNetMembershipUser user, string normalizedEmail, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.NormalizedEmail = normalizedEmail); 
    } 

    public Task SetNormalizedUserNameAsync(AspNetMembershipUser user, string normalizedName, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.NormalizedUserName = normalizedName); 
    } 

    public Task SetPasswordHashAsync(AspNetMembershipUser user, string passwordHash, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.PasswordHash = passwordHash); 
    } 

    public Task SetUserNameAsync(AspNetMembershipUser user, string userName, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => user.UserName = userName); 
    } 

    public Task<IdentityResult> UpdateAsync(AspNetMembershipUser user, CancellationToken cancellationToken) 
    { 
     return Task.Factory.StartNew(() => 
     { 
      try 
      { 
       User dbUser = _dbcontext.Users 
        .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership) 
        .Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication) 
        .Include(u => u.UserGroups) 
        .SingleOrDefault(u => u.UserId.ToString() == user.Id); 

       if (dbUser != null) 
       { 
        this.Convert(user, dbUser); 
        _dbcontext.Users.Update(dbUser); 
        _dbcontext.SaveChanges(); 
       } 
       return IdentityResult.Success; 
      } 
      catch(Exception ex) 
      { 
       return IdentityResult.Failed(new IdentityError 
       { 
        Code = ex.GetType().Name, 
        Description = ex.Message 
       }); 
      } 
     }); 
    } 

    private void Convert(User from, AspNetMembershipUser to) 
    { 
     to.Id = from.ProviderUserKey.ToString(); 
     to.UserName = from.ProviderUserName; 
     to.NormalizedUserName = from.ProviderUserName.ToLower(); 
     to.Email = from.ProviderEmailAddress; 
     to.NormalizedEmail = from.ProviderEmailAddress.ToLower(); 
     to.EmailConfirmed = true; 
     to.PasswordHash = from.AspNetUser.AspNetMembership.Password; 
     to.PasswordSalt = from.AspNetUser.AspNetMembership.PasswordSalt; 
     to.PasswordFormat = from.AspNetUser.AspNetMembership.PasswordFormat; 
     to.AccessFailedCount = from.AspNetUser.AspNetMembership.FailedPasswordAttemptCount; 
     to.EmailConfirmed = true; 
     to.Roles.Clear(); 
     from.UserGroups.ToList().ForEach(ug => 
     { 
      to.Roles.Add(new IdentityUserRole<string> 
      { 
       RoleId = ug.GroupId.ToString(), 
       UserId = ug.UserId.ToString() 
      }); 
     }); 
     to.PhoneNumber = from.Phone ?? from.ShippingPhone; 
     to.PhoneNumberConfirmed = !string.IsNullOrEmpty(to.PhoneNumber); 
     to.SecurityStamp = from.AspNetUser.AspNetMembership.PasswordSalt; 
    } 

    private void Convert(AspNetMembershipUser from , User to) 
    { 
     AspNetApplication application = _dbcontext.AspNetApplications.First(); 

     to.ProviderUserKey = Guid.Parse(from.Id); 
     to.ProviderUserName = from.UserName; 
     to.ProviderEmailAddress = from.Email; 
     to.InternalEmail = $"c_{Guid.NewGuid().ToString()}@mycompany.com"; 
     to.AccountOwner = "MYCOMPANY"; 
     to.UserStatusId = (int)UserStatus.Normal; 

     AspNetUser aspNetUser = to.AspNetUser; 

     if (to.AspNetUser == null) 
     { 
      to.AspNetUser = new AspNetUser 
      { 
       ApplicationId = application.ApplicationId, 
       AspNetApplication= application, 
       AspNetMembership = new AspNetMembership 
       { 
        ApplicationId = application.ApplicationId, 
        AspNetApplication = application 
       } 
      }; 
     } 

     to.AspNetUser.UserId = Guid.Parse(from.Id); 
     to.AspNetUser.UserName = from.UserName; 
     to.AspNetUser.LoweredUserName = from.UserName.ToLower(); 
     to.AspNetUser.LastActivityDate = DateTime.UtcNow; 
     to.AspNetUser.IsAnonymous = false; 
     to.AspNetUser.ApplicationId = application.ApplicationId; 
     to.AspNetUser.AspNetMembership.CreateDate = DateTime.UtcNow; 
     to.AspNetUser.AspNetMembership.Email = from.Email; 
     to.AspNetUser.AspNetMembership.IsApproved = true; 
     to.AspNetUser.AspNetMembership.LastLoginDate = DateTime.Parse("1754-01-01 00:00:00.000"); 
     to.AspNetUser.AspNetMembership.LastLockoutDate = DateTime.Parse("1754-01-01 00:00:00.000"); 
     to.AspNetUser.AspNetMembership.LastPasswordChangedDate = DateTime.Parse("1754-01-01 00:00:00.000"); 
     to.AspNetUser.AspNetMembership.LoweredEmail = from.NormalizedEmail.ToLower(); 
     to.AspNetUser.AspNetMembership.Password = from.PasswordHash; 
     to.AspNetUser.AspNetMembership.PasswordSalt = from.PasswordSalt; 
     to.AspNetUser.AspNetMembership.PasswordFormat = from.PasswordFormat; 
     to.AspNetUser.AspNetMembership.IsLockedOut = false; 
     to.AspNetUser.AspNetMembership.FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000"); 
     to.AspNetUser.AspNetMembership.FailedPasswordAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000"); 

     // Merge Groups/Roles 
     to.UserGroups 
      .Where(ug => !from.Roles.Any(r => ug.GroupId.ToString() == r.RoleId)) 
      .ToList() 
      .ForEach(ug => to.UserGroups.Remove(ug)); 

     to.UserGroups 
      .Join(from.Roles, ug => ug.GroupId.ToString(), r => r.RoleId, (ug, r) => new { To = ug, From = r }) 
      .ToList() 
      .ForEach(j => 
      { 
       j.To.UserId = long.Parse(j.From.UserId); 
       j.To.GroupId = int.Parse(j.From.RoleId); 
      }); 

     from.Roles 
      .Where(r => !to.UserGroups.Any(ug => ug.GroupId.ToString() == r.RoleId)) 
      .ToList() 
      .ForEach(r => 
      { 
       to.UserGroups.Add(new UserGroup 
       { 
        UserId = long.Parse(from.Id), 
        GroupId = int.Parse(r.RoleId) 
       }); 
      }); 
    } 
} 

AspNetMembershipPasswordHasher.cs

public class AspNetMembershipPasswordHasher : IPasswordHasher<AspNetMembershipUser> 
{ 
    private readonly int _saltSize; 
    private readonly int _bytesRequired; 
    private readonly int _iterations; 

    public AspNetMembershipPasswordHasher() 
    { 
     this._saltSize = 128/8; 
     this._bytesRequired = 32; 
     this._iterations = 1000; 
    } 

    public string HashPassword(AspNetMembershipUser user, string password) 
    { 
     string passwordHash = null; 
     string passwordSalt = null; 

     this.HashPassword(password, out passwordHash, ref passwordSalt); 

     user.PasswordSalt = passwordSalt; 
     return passwordHash; 
    } 

    public PasswordVerificationResult VerifyHashedPassword(AspNetMembershipUser user, string hashedPassword, string providedPassword) 
    { 
     // Throw an error if any of our passwords are null 
     if (hashedPassword == null) 
     { 
      throw new ArgumentNullException("hashedPassword"); 
     } 

     if (providedPassword == null) 
     { 
      throw new ArgumentNullException("providedPassword"); 
     } 

     string providedPasswordHash = null; 

     if (user.PasswordFormat == 0) 
     { 
      providedPasswordHash = providedPassword; 
     } 
     else if (user.PasswordFormat == 1) 
     { 

      string providedPasswordSalt = user.PasswordSalt; 

      this.HashPassword(providedPassword, out providedPasswordHash, ref providedPasswordSalt); 
     } 
     else 
     { 
      throw new NotSupportedException("Encrypted passwords are not supported."); 
     } 

     if (providedPasswordHash == hashedPassword) 
     { 
      return PasswordVerificationResult.Success; 
     } 
     else 
     { 
      return PasswordVerificationResult.Failed; 
     } 
    } 

    private void HashPassword(string password, out string passwordHash, ref string passwordSalt) 
    { 
     byte[] hashBytes = null; 
     byte[] saltBytes = null; 
     byte[] totalBytes = new byte[this._saltSize + this._bytesRequired]; 

     if (!string.IsNullOrEmpty(passwordSalt)) 
     { 
      // Using existing salt. 
      using (var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(passwordSalt), this._iterations)) 
      { 
       saltBytes = pbkdf2.Salt; 
       hashBytes = pbkdf2.GetBytes(this._bytesRequired); 
      } 
     } 
     else 
     { 
      // Generate a new salt. 
      using (var pbkdf2 = new Rfc2898DeriveBytes(password, this._saltSize, this._iterations)) 
      { 
       saltBytes = pbkdf2.Salt; 
       hashBytes = pbkdf2.GetBytes(this._bytesRequired); 
      } 
     } 

     Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, this._saltSize); 
     Buffer.BlockCopy(hashBytes, 0, totalBytes, this._saltSize, this._bytesRequired); 

     using (SHA256 hashAlgorithm = SHA256.Create()) 
     { 
      passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes)); 
      passwordSalt = Convert.ToBase64String(saltBytes); 
     } 
    } 
} 

Répondre

1

Un de mes collègues a pu me aider. Voici à quoi devrait ressembler la fonction de hachage. Avec cette modification, ASP.NET Identity est capable de se greffer sur une base de données d'adhésion ASP.NET existante.

private void HashPassword(string password, out string passwordHash, ref string passwordSalt) 
    { 
     byte[] passwordBytes = Encoding.Unicode.GetBytes(password); 
     byte[] saltBytes = null; 

     if (!string.IsNullOrEmpty(passwordSalt)) 
     { 
      saltBytes = Convert.FromBase64String(passwordSalt); 
     } 
     else 
     { 
      saltBytes = new byte[128/8]; 
      using (var rng = RandomNumberGenerator.Create()) 
      { 
       rng.GetBytes(saltBytes); 
      } 
     } 

     byte[] totalBytes = new byte[saltBytes.Length + passwordBytes.Length]; 
     Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, saltBytes.Length); 
     Buffer.BlockCopy(passwordBytes, 0, totalBytes, saltBytes.Length, passwordBytes.Length); 

     using (SHA1 hashAlgorithm = SHA1.Create()) 
     { 
      passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes)); 
     } 

     passwordSalt = Convert.ToBase64String(saltBytes); 
    } 

Vous pouvez trouver tous les source code on GitHib.