Implementing User and Password Migration During Site Migration
User migration is technically complex process due to incompatibility of password hashing algorithms between different platforms. Correct approach prevents users from needing password reset.
Password Hash Incompatibility Problem
| Platform | Algorithm | Format |
|---|---|---|
| WordPress | phpass (md5-based) | $P$BHash... |
| Drupal 7 | sha512 + salt | $S$5Hash... |
| Laravel | bcrypt | $2y$10$Hash... |
| Django | PBKDF2 SHA256 | pbkdf2_sha256$N$salt$hash |
| PHP legacy | MD5 | 32 hex chars |
| bcrypt | bcrypt | $2a$10$Hash... |
Strategy 1: Lazy Migration (Preferred)
Hashes transferred as-is. On first user login, old algorithm checked; if success, hash re-hashed with new algorithm.
# models/user.py
class User(BaseModel):
password_hash: str
password_algorithm: str # 'bcrypt', 'phpass', 'sha512', 'legacy_md5'
def verify_password(self, plain_password: str) -> bool:
if self.password_algorithm == 'bcrypt':
return bcrypt.checkpw(plain_password.encode(), self.password_hash.encode())
elif self.password_algorithm == 'phpass':
return phpass_check(plain_password, self.password_hash)
elif self.password_algorithm == 'legacy_md5':
return hashlib.md5(plain_password.encode()).hexdigest() == self.password_hash
return False
def upgrade_password_hash(self, plain_password: str):
"""Re-hash on successful login"""
new_hash = bcrypt.hashpw(plain_password.encode(), bcrypt.gensalt(rounds=12))
self.password_hash = new_hash.decode()
self.password_algorithm = 'bcrypt'
db.save(self)
Login handling:
def login(email: str, password: str):
user = db.get_user_by_email(email)
if not user:
return None
if user.verify_password(password):
# Update hash if using legacy algorithm
if user.password_algorithm != 'bcrypt':
user.upgrade_password_hash(password)
return create_session(user)
return None
ETL: User Transfer
def migrate_users_from_wordpress(wp_db, new_db):
cursor = wp_db.cursor(dictionary=True)
cursor.execute("""
SELECT
u.ID, u.user_login, u.user_pass, u.user_email,
u.user_registered, u.display_name,
um.meta_value as first_name
FROM wp_users u
LEFT JOIN wp_usermeta um ON u.ID = um.user_id AND um.meta_key = 'first_name'
ORDER BY u.ID
""")
migrated = 0
skipped = 0
for wp_user in cursor.fetchall():
# Check: already migrated?
existing = new_db.get_user_by_email(wp_user['user_email'])
if existing:
skipped += 1
continue
algorithm = detect_wp_hash_algorithm(wp_user['user_pass'])
new_db.create_user({
'username': wp_user['user_login'],
'email': wp_user['user_email'],
'password_hash': wp_user['user_pass'],
'password_algorithm': algorithm,
'display_name': wp_user['display_name'],
'created_at': wp_user['user_registered'],
'legacy_id': wp_user['ID'],
})
migrated += 1
print(f"Migrated: {migrated}, Skipped: {skipped}")
def detect_wp_hash_algorithm(hash_val):
if hash_val.startswith('$P$') or hash_val.startswith('$H$'):
return 'phpass'
if hash_val.startswith('$2y$') or hash_val.startswith('$2a$'):
return 'bcrypt'
if len(hash_val) == 32:
return 'legacy_md5'
return 'unknown'
Force Password Reset for Legacy Algorithms
If legacy support not desired — notify users of reset:
def send_password_reset_for_legacy_users():
users = db.query(
"SELECT * FROM users WHERE password_algorithm IN ('legacy_md5', 'sha1')"
)
for user in users:
token = generate_secure_token()
db.save_reset_token(user.id, token, expires_in=7*24*3600)
send_email(
to=user.email,
subject="Update password required",
template="password_reset_migration",
vars={
'name': user.display_name,
'reset_url': f"https://site.com/reset?token={token}",
'deadline': '7 days'
}
)
Roles and Permissions
ROLE_MAP = {
# WordPress → Custom CMS
'administrator': 'admin',
'editor': 'editor',
'author': 'author',
'contributor': 'contributor',
'subscriber': 'user',
}
def migrate_user_roles(wp_db, new_db):
cursor = wp_db.cursor(dictionary=True)
cursor.execute("""
SELECT user_id, meta_value as capabilities
FROM wp_usermeta WHERE meta_key = 'wp_capabilities'
""")
for row in cursor.fetchall():
caps = php_unserialize(row['capabilities'])
wp_role = list(caps.keys())[0] if caps else 'subscriber'
new_role = ROLE_MAP.get(wp_role, 'user')
new_db.update_user_role(row['user_id'], new_role)
Execution Time
User migration with lazy password migration and role mapping — 2–3 working days.







