Silently migrating users from EPiServer 4.x to CMS 5

by: Johan Olofsson

This is an effort to work around the problem with migrating EPiServer 4.x users to the new Membership/Role-provider architecture used by EPiServer CMS5 without having to reset all the users passwords. (Passwords are stored as a calculated hashed value in EPiServer 4.x, so there is no way to retrieve them in clear text).

I’ve come up with a small “migrating”-membership provider which will silently migrate each user “on the fly” the next time he/she logs in.

The “migrating” provider is simply installed as the last provider “in the chain” under the Multiplexing provider (which is required) and is given the connectionString to the old EPiServer 4.x database (which is also required):

   1: <membership defaultProvider="MultiplexingMembershipProvider" userIsOnlineTimeWindow="10">
   2:   <providers>
   3:     <clear />
   4:     <add name="MultiplexingMembershipProvider" type="EPiServer.Security.MultiplexingMembershipProvider, EPiServer" 
   5:          provider1="SqlServerMembershipProvider" 
   6:          provider2="MigratingProvider" />
   7:     <add name="SqlServerMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="EPiServerDB" requiresQuestionAndAnswer="false" applicationName="EPiServerSample" requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />
   8:     <add name="MigratingProvider"
   9:          type="EPiServerCMS5.MigrateUsers.MigratingMembershipProvider, EPiServerCMS5.MigrateUsers"
  10:          migrateToProvider="SqlServerMembershipProvider"
  11:          migrateRolesToProvider="SqlServerRoleProvider"
  12:          connectionString="Data Source=(local)\sqlexpress;Database=dbEPiServer4;User Id=user;Password=pwd;Network Library=DBMSSOCN;"
  13:   />
  14:   </providers>
  15: </membership>

 

Now when an “old” user tries to logon the migrating provider will be called in its GetUser()-implementation, which will return a MembershipUser if the user can be found in the old EPiServer 4.x database.

   1: public override MembershipUser GetUser(string username, bool userIsOnline)
   2: {
   3:     if( null==currentMigratingUser || currentMigratingUser.UserName != username )
   4:         currentMigratingUser = InternalGetUser(username);
   5:  
   6:     return currentMigratingUser;
   7: }

 

The code to actually do the database read is implemented in the InternalGetUser()-method, as it could be called from both the GetUser() and ValidateUser() methods. Note also that the method returns a MigratingUser instance, a class derived from MembershipUser that adds a few more fields needed when performing password validation in the ValidateUser()-method. It will also contain the groups the user belongs to, and the reason for this is that we simply can keep the needed database calls down to just one for each user.

   1: protected MigratingUser InternalGetUser(string username)
   2: {
   3:     MigratingUser user = null;
   4:  
   5:     SqlConnection c = new SqlConnection(connectionString);
   6:     c.Open();
   7:  
   8:     try
   9:     {
  10:         SqlCommand cmd = new SqlCommand(@"
  11:  
  12: blSID.pkId, Email, Description, Salt, Hash from tblSID left join tblUser on fkSID=tblSID.pkId where Name=@name and Type=2
  13:  
  14: Group].Name from tblSid as [Group] inner join tblSidGroup on fkSIDParent=[Group].pkId inner join tblSid as [User] on tblSIDGroup.fkSID=[User].pkId where [User].Name=@name and [User].Type=2
  15: ", c);
  16:  
  17:         cmd.CommandType = CommandType.Text;
  18:         cmd.Parameters.Add("@name", SqlDbType.NVarChar, 450).Value = username;
  19:         SqlDataReader r = cmd.ExecuteReader();
  20:  
  21:         if( r.Read() )
  22:         {
  23:             user = new MigratingUser(ApplicationName, username, r["Email"] as string, r["Description"] as string);
  24:             
  25:             user.Salt = r["Salt"] as byte[];
  26:             user.Hash = r["Hash"] as byte[];
  27:  
  28:             if(r.NextResult())
  29:             {
  30:                 while(r.Read())
  31:                 {
  32:                     user.Roles.Add(r["Name"] as string);
  33:                 }
  34:             }
  35:         }
  36:  
  37:         r.Dispose();
  38:     }
  39:     finally
  40:     {
  41:         c.Close();
  42:     }
  43:  
  44:     return user;
  45: }

 

Then lastly in the ValidateUser()-implementation the password is validated by calculating and comparing the “hash” (in the same way as was done in EPiServer 4.x) with whats stored in the old database. If the two hashes match, a new user is created in the MemberhipProvider pointed out by the MigrateToProvider-setting. Also, all groups that the user belongs to are migrated to the RoleProvider pointed out by the MigrateRolesToProvider-setting. Note that the groups have to exist in the provider!!

And as a last step, we reset the MultiplexingProvider.CurrentMembershipUser to the newly created user and return success.

   1: public override bool ValidateUser(string username, string password)
   2: {
   3:     bool status = false;
   4:  
   5:     if( null==currentMigratingUser || currentMigratingUser.UserName!=username )
   6:         currentMigratingUser = InternalGetUser(username);
   7:  
   8:     if(null==currentMigratingUser)
   9:         return false;
  10:  
  11:     if(VerifyPassword(currentMigratingUser.Salt, currentMigratingUser.Hash, password))
  12:     {
  13:         // create user in the MigrateToProvider
  14:         MembershipCreateStatus createStatus;
  15:         MembershipUser user = MigrateToProvider.CreateUser(currentMigratingUser.UserName, password, currentMigratingUser.Email, null, null, true, null, out createStatus);
  16:  
  17:         status = createStatus == MembershipCreateStatus.Success;
  18:  
  19:         if(status)
  20:         {
  21:             // migrate roles
  22:             List<string> rolesToMigrate = new List<string>(currentMigratingUser.Roles);
  23:  
  24:             foreach( string role in currentMigratingUser.Roles )
  25:             {
  26:                 // remove roles that dont exist in the roleprovider
  27:                 if(!MigrateRolesToProvider.RoleExists(role))
  28:                     rolesToMigrate.Remove(role);
  29:             }
  30:  
  31:             MigrateRolesToProvider.AddUsersToRoles(new string[] { currentMigratingUser.UserName }, rolesToMigrate.ToArray());
  32:  
  33:  
  34:             // reset current user at multiplexingprovider so its the newly created user thats considered logged in
  35:             // (and not the user returned by our MigratingMembershipUser.GetUser()-implementation
  36:             // this is to ensure that upcoming calls on RoleProvider succeeds in getting the roles for the user
  37:             MultiplexingMembershipProvider mp = Membership.Provider as MultiplexingMembershipProvider;
  38:             if(null!=mp)
  39:                 mp.CurrentMembershipUser = user;
  40:         }
  41:     }
  42:  
  43:     return status;
  44: }

 

You should be aware of that this is a somewhat experimental project that hasn’t undergone any extensive testing, but I release it with full sources: http://labs.episerver.com/PageFiles/111555/EPiServerCMS5.MigrateUsers.zip (14Kb)

Regards,
Johan

06 October 2008


Comments

  1. Great work! It this is taken to production quality, we have a solution to the v4 to v5 db user problem, which is one of the missing pieces in the migration tool.
  2. We will add user migration to the migration tool also. But as Johan points out there is no way for migration tool to preserve passwords so in that solution passwords will be regenerated. But the great thing is that now there will be several options for migration projects, they can either choose to migrate users during migration process (with passwords reset) or they can use this approach to migrate users afterwards. Great!
  3. I would like to know exactely Episerver4 is told in plain English as I don't understand why it sometimes jumps up on my screen. With regards Helle Hansen Denmark
Post a comment    
User verification Image for user verification  
EPiTrace logger