Skip to content

Instantly share code, notes, and snippets.

@AcckiyGerman
Last active January 31, 2019 06:37
Show Gist options
  • Save AcckiyGerman/f9afef2ab1f43317cd2ec1d79bf6bde4 to your computer and use it in GitHub Desktop.
Save AcckiyGerman/f9afef2ab1f43317cd2ec1d79bf6bde4 to your computer and use it in GitHub Desktop.
Joomla users in Django (Django auth backend, that populates users from Joomla)

Joomla users in Django (Django auth backend, that populates users from Joomla)

Once I was in need to use our existing Joomla users in my new API, written in Django. Problem is that I could not just copy Joomla users into a Django database, because

  • Joomla password hashing system differs from Django one.
  • J-users and D-users had different set of fields (this is easy to fix, but still)

So instead I made a custom auth backend for Django, and now I can confidently say that Django can authenticate users against the Joomla database, without need to decrypt password hashes.

Algorithm:

  • connect the Joomla database to the Django project
  • create JoomlaUser model, to populate users from the Joomla DB
  • implement check_joomla_password() function, that validates user passwords the same way as Joomla
  • add custom "Joomla Auth Backend" that copies each user from Joomla to Django at the first login

Implementation:

To understand what's going on, you should have some experience with Django. The code have to be modified accordingly to your django project. However the code is taken from the working project with minimum changes, and it should be easy to set up for your needs.

1. connect to Joomla DB:

DATABASES = {
    'default': {"your default DB settings"},

    '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',
    }
}

# ensure that Joomla users are populated from the right database:
DATABASE_ROUTERS = ['manager.router.DatabaseAppsRouter']
DATABASE_APPS_MAPPING = {'joomla_users': 'joomla_db'}
# you also can create your own database router:
# https://docs.djangoproject.com/en/dev/topics/db/multi-db/#automatic-database-routing

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

2. create Joomla user model

  • Read https://docs.djangoproject.com/en/2.1/howto/legacy-databases/
  • Think where to keep new "Joomla user" model. In my project I've created 'users' app, where my custom user models live, and the custom Joomla backend will be placed.
  • inspect how the user is stored in the existing Joomla DB: python manage.py inspectdb --database="joomla_db"
  • Find and carefully examine the users table.
  • Add 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"  

Now go to django shell ./manage.py shell and try to populate some users, e.g.

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

If everything works - move on to the next step. Otherwise look into errors, fix settings, etc

3. Check Joomla user passwords

Joomla does not store user password, but the password hash, e.g. $2y$10$aoZ4/bA7pe.QvjTU0R5.IeFGYrGag/THGvgKpoTk6bTz6XNkY0F2e

Starting from Joomla v3.2, user passwords are hashed using BLOWFISH algorithm.

So I've downloaded a python blowfish implementation:

pip install bcrypt
echo bcrypt >> requirements.txt

And created Joomla password check function in the 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 from Joomla.
    
    :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)

Old versions Warning! Joomla < 3.2 uses different hashing method (md5+salt), so this function won't work. In this case read https://stackoverflow.com/questions/10428126/joomla-password-encryption and implement a hash checker in python, which probably will look something like:

# WARNING - THIS FUNCTION 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 have no old Joomla instance running, thus I couldn't test this function for you.

4. Joomla Authentication Backend

Now you are ready to create a Joomla authentication backend for Django.

  1. read how to modify django auth backends: https://docs.djangoproject.com/en/dev/topics/auth/customizing/

  2. Register Jango (not yet existing) backend in the 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',
]
  1. Create Joomla authentication Backend in users/backend.py:
from django.contrib.auth.models import User
from .models import JoomlaUser

""" check password function we wrote before """
def check_joomla_password(password, hashed):
    ...


class JoomlaBackend:
    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 identical Django user:
            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

Test & documentation

Congratulations - now your customers from old Joomla site can use their credentials on the new Django site or rest api, etc

Now, add proper tests and documentation to cover this new code. It's logic is quite tricky, so if you won't make tests&docs (lazy dude) - maintaining the project will be a pain in your (or somebody's else) ass.

Kind regards, @ Dmytro Gierman

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment