Back to Home

Using Joomla Accounts in a Django Project

django · joomla · web development · account · user migration

Using Joomla Accounts in a Django Project

Let's say that the site your users use is written in Joomla, but to create a new product for your audience, you chose the Python / Django bundle.


As a result, you need to use user accounts from the Joomla database in Django.


The problem, however, is that Joomla and Django use different password hashing algorithms, so just copying the accounts fails.


After reading the Django documentation, stack overflow and spending some time, I got the solution described below, which uses the recommended development practices for Django to the maximum.


Warnings


This architectural solution may not suit you, see the discussion in the comments .


To understand what happens in the examples below, you must have some understanding of Django architecture.


I also assume that you know how to deploy a Django project, so I am not describing this process.


The code is copied from a working project, but it will be easy to adjust to your project with a minimum of changes.


Probably, in the next major version of Django, this code may break, however, the principle of solution will remain the same.


In this guide, I do not describe the front end of the authorization system, since:


  • what front-end you have will depend on the needs of your project (it may even be a Json API endpoint, for example)
  • this information is already described in the official Django tutorials and various starter articles

Algorithm


  • connect the Joomla database (DB) to the Django project
  • create a JoomlaUser model representing a user from the Joomla database
  • write a function check_joomla_password()that verifies that the entered password matches the user's original password.
  • add a new authorization backend "Joomla Auth Backend" to the project, which, when authorizing the client in Django, will get the user account from the Joomla database

1. Connection to Joomla database:


  • Read how Django works with multiple databases
  • To connect the Joomla database to our Django project, add the following code to the project settings file /project_name/settings.py:


    DATABASES = {
    # БД по умолчанию 
    'default': {
        ...
    },
    'joomla_db': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {},
        'NAME': 'joomla_database_name',
        # Don't store passwords in the code, instead use env vars:
        'USER':     os.environ['joomla_db_user'],
        'PASSWORD': os.environ['joomla_db_pass'],
        'HOST': 'joomla_db_host, can be localhost or remote IP',
        'PORT': '3306',
    }
    }


If necessary, in the same file with the project settings, you can enable logging of database queries:


# add logging to see DB requests:
LOGGING = {
    'version': 1,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console'],
        },
    },
}

2. create a JoomlaUser model


  • Read how a Django model can use an existing database
  • Think about where to place the new JoomlaUser.
    In my project, I created an application called "users" ( manage.py startapp users). It will contain the authorization backend and the Joomla user model.
  • generate the model automatically using inspectdb:
    python manage.py inspectdb live_users --database="joomla_db"
    joomla_db - the name of the database that you specified in settings.py/DATABASES;
    live_users - name of the table with accounts.
  • add your model to users/models.py:


    class JoomlaUser(models.Model):
    """ Represents our customer from the legacy Joomla database. """
    username = models.CharField(max_length=150, primary_key=True)
    email = models.CharField(max_length=100)
    password = models.CharField(max_length=100)
    # you can copy more fields from `inspectdb` output, 
    # but it's enough for the example
    class Meta:
        # joomla db user table. WARNING, your case can differs.
        db_table = 'live_users'
        # readonly 
        managed = False
        # tip for the database router
        app_label = "joomla_users"  


Next, we need to make sure that the model will access the correct database. To do this, add to the project a router for queries to different databases , which will redirect requests from the JoomlaUser model to its native database.


  1. Create the file "db_routers.py" in the main folder of the project (in the same place where your "settings.py" is):


    # project_name/db_routers.py
    class DbRouter:
    """this router makes sure that django uses legacy 'Joomla' database for models,
    that are stored there (JoomlaUser)"""
    def db_for_read(self, model, **kwargs):
        if model._meta.app_label == 'joomla_user':
            return 'joomla_db'
        return None
    def db_for_write(self, model, **kwargs):
        if model._meta.app_label == 'joomla_user':
            return 'joomla_db'
        return None

  2. register a new router in settings.py:


    # ensure that Joomla users are populated from the right database:
    DATABASE_ROUTERS = ['project_name.db_routers.DbRouter']


Now you can get an account from the old database.
Launch a Django terminal and try pulling an existing user:python manage.py shell


>>> from users.models import JoomlaUser
>>> print(JoomlaUser.objects.get(username='someuser'))
JoomlaUser object (someusername)
>>> 

If everything works (you see the user), then go to the next step. Otherwise, look at the error output and correct the settings.


3. Verify Joomla Account Password


Joomla does not store user passwords, but their hash, for example
$2y$10$aoZ4/bA7pe.QvjTU0R5.IeFGYrGag/THGvgKpoTk6bTz6XNkY0F2e


Starting with Joomla v3.2, user passwords are encrypted using the BLOWFISH algorithm .


So I downloaded python code with this algorithm:


pip install bcrypt
echo bcrypt >> requirements.txt

And created a function to check passwords in a file users/backend.py:


def check_joomla_password(password, hashed):
    """
    Check if password matches the hashed password,
    using same hashing method (Blowfish) as Joomla >= 3.2
    If you get wrong results with this function, check that
    the Hash starts from prefix "$2y", otherwise it is 
    probably not a blowfish hash
    :return: True/False
    """
    import bcrypt
    if password is None:
        return False
    # bcrypt requires byte strings
    password = password.encode('utf-8')
    hashed = hashed.encode('utf-8')
    return hashed == bcrypt.hashpw(password, hashed)

Attention! Joomla versions lower than 3.2 use a different hashing method (md5 + salt), so this function will not work. In that case, read the
discussion on Stackoverflow and create a hash check function that looks something like this:


# WARNING - THIS FUNCTION WAS NOT TESTED WITH REAL JOOMLA USERS
# and definitely has some errors
def check_old_joomla_password(password, hashed):
    from hashlib import md5
    password = password.encode('utf-8')
    hashed = hashed.encode('utf-8')
    if password is None:
        return False
    # check carefully this part:
    hash, salt = hashed.split(':')
    return hash == md5(password+salt).hexdigest()

Unfortunately, I don’t have a user base from the old version of Joomla at hand, so I can’t test this feature for you.


4. Backend user authorization Joomla


Now you are ready to create a Django backend for authorizing users from the Joomla project.


  1. прочитайте, как модифицировать систему авторизации Django


  2. Зарегистрируйте новый бэкенд (еще не существующий) в project/settings.py:


    AUTHENTICATION_BACKENDS = [
    # Check if user already in the local DB
    # by using default django users backend
    'django.contrib.auth.backends.ModelBackend',
    # If user was not found among django users,
    # use Joomla backend, which:
    #   - search for user in Joomla DB
    #   - check joomla user password
    #   - copy joomla user into Django user.
    'users.backend.JoomlaBackend',
    ]

  3. Создайте бэкенд авторизации пользователей Joomla в users/backend.py



from django.contrib.auth.models import User
from .models import JoomlaUser
def check_joomla_password(password, hashed):
    # this is a fuction, that we wrote before
    ...
class JoomlaBackend:
    """ authorize users against Joomla user records """
    def authenticate(self, request, username=None, password=None):
        """
        IF joomla user exists AND password is correct:
            create django user
            return user object 
        ELSE:
            return None
        """
        try:
            joomla_user = JoomlaUser.objects.get(username=username)
        except JoomlaUser.DoesNotExist:
            return None
        if check_joomla_password(password, joomla_user.password):
            # Password is correct, let's create and return Django user,
            # identical to Joomla user:
            # but before let's ensure there is no same username
            # in DB. That could happen, when user changed password
            # in Joomla, but Django doesn't know that
            User.objects.filter(username=username).delete()  
            return User.objects.create_user(
                username=username,
                email=joomla_user.email,
                password=password,
                # any additional fields from the Joomla user:
                ...
            )
    # this method is required to match Django Auth Backend interface
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Итог


Поздравляю — теперь пользователи вашего существующего Joomla сайта могут использовать свои учётные данные на новом сайте/приложении.


По мере авторизации активных пользователей через новый интерфейс, они будут по одному скопированы в новую базу данных.


Как вариант, вы можете не захотеть копировать сущности пользователей из старой системы в новую.


В таком случае вот вам ссылка на статью, в которой описывается, как в Django заменить модель пользователя по умолчанию на свою (вышеописанную модель JoomlaUser).


The final decision, whether or not to transfer users, is made on the basis of the relationship in which the new and old projects will be. For example, where will the registration of new users take place, which site / application will be the main one, etc.


Testing and documentation


Now please add the appropriate tests and documentation covering the new code. The logic of this solution is closely intertwined with the Django architecture and is not very obvious, so if you do not do the tests / documentation now, support for the project will become more complicated in the future.

Read Next