J'ai été confronté à ce problème il y a quelque temps (migration d'un site PHP vers django) et nous ne voulions pas obliger l'ensemble des utilisateurs à changer leur mot de passe (les passwords étaient salés avec une clé fixe en SHA puis hashés).
Pour résoudre cette situation, nous avons simplement créé notre propre AUTHENTICATION_BACKEND
, qui se charge d'authentifier les utilisateurs dont le mot de passe n'a pas été changé depuis la migration.
Sur le profil utilisateur, on rajoute également un champ password_changed_date
et un champ legacy_id
qui contient l'ID initial de l'utilisateur. Cela permet de n'utiliser le backend que lorsque nécessaire.
Concrètement, le code ressemble à ce qui suit.
Pour la partie profil:
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save
from django.utils import timezone
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile')
legacy_id = models.IntegerField(null=True, blank=True, unique=True)
legacy_password_change_date = models.DateTimeField(null=True, blank=True)
def update_change_password_date(sender, instance, **kwargs):
# if model is just created, we do nothing
try:
db_user = get_user_model().objects.get(username=instance.username)
except get_user_model().DoesNotExist:
return
# password has changed, we update the change date
if db_user.password != instance.password:
instance.profile.password_change_date = timezone.now()
instance.profile.save()
pre_save.connect(update_change_password_date, sender=settings.AUTH_USER_MODEL)
Pour le backend:
from __future__ import unicode_literals
import hashlib
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
def check_hash(user, password):
"""Notre fonction legacy de vérification de mot de passe
hashed_password = something # à toi d'implémenter ton hashage legacy
return user.password == hashed_password
class LegacyModelBackend(ModelBackend):
"""l'essentiel du code est copié depuis le ModelBackend de django"""
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
# on vérifie que l'utilisateur est bien legacy est n'a jamais changé son mot de passe
if user.profile.legacy_id and user.profile.password_changed_date is None:
if check_hash(user, password):
return user
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
Dans les settings, ils te reste à rajouter ton backend:
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'legacy.backends.LegacyModelBackend',
)
Chez nous, ça marche bien, même couplé à d'autres solutions (comme django-allauth, pour l'authentification OAuth).
Il me semble que c'est une solution plutôt propre pour gérer la situation, même si tu peux choisir d'implémenter les choses différement.
Par exemple, si tu utilise déjà un modèle utilisateur personnalisé, pas besoin de t'embéter à créer un profil. Tu peux mettre les champs directement sur le modèle utilisateur.
Bien entendu, ça reste toujours moins propre que de réinitialiser tous les mot de passe et de partir sur une base clean, puisque cela alourdit le code. Néanmoins, repartir à zéro n'est pas toujours envisageable.