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.
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 Noneregister 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.txtAnd 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.
прочитайте, как модифицировать систему авторизации Django
Зарегистрируйте новый бэкенд (еще не существующий) в
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', ]Создайте бэкенд авторизации пользователей 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.