I'm overhauling a site I'd originally made using Joomla to Django, and I was wondering if I can import the user records directly from Joomla (my main concern is the user passwords as they are encrypted).
Yes, you can, but you'll have to do some work. Joomla keeps users in some specific DB table structure, so you'll have to pull them out and insert them into a users table you create in your Django application. As for encryption, if the algorithm is known, it's probably the hash value that's kept in the DB, and you can just transfer it as-is as long as you implement the same hashing algorithm in your Django application.
Remember: Django is a more general 'concept' than Joomla - it's a framework for writing web application, hence in theory you can even re-implement Joomla completely with it.
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 or to copy all users from Joomla DB at once.
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:
Read https://docs.djangoproject.com/en/dev/topics/db/multi-db/
Add to /project_name/settings.py:
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',
}
}
# 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 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"
To ensure, that JoomlaUser model will use right DB, add a database router:
Create file "db_routers.py" in the project folder, where the "settings.py" file is stored:
# 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_users':
return 'joomla_db'
return None
def db_for_write(self, model, **kwargs):
if model._meta.app_label == 'joomla_users':
return 'joomla_db'
return None
register new router, for that, add in settings.py:
# ensure that Joomla users are populated from the right database:
DATABASE_ROUTERS = ['project_name.db_routers.DbRouter']
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 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.
read how to modify django auth backends: https://docs.djangoproject.com/en/dev/topics/auth/customizing/
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',
]
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
Update 11.04.2019 - errors fixed.
I think there is 3 ways to approach this problem:
1) You can read about how joomla and django make hash of passwords and make the migration with a script
2) You can make your own authentication backend
3) You can use a ETL tool
Joomla (PHP) is a CMS while Django (Python) is a web framework.
I wonder whether this is really possible. What i can conclude at this point of time is that it is not possible. However someone may have any idea about this.
Thanks :)
Related
I have about 6 target databases with their own Django admin data and application data. Right now, I am running 6 instances of the same Django application on different hosts. What I would like to do is run one instance of the app, but up front have the user select which database he wishes to connect. Where should I inject this new code so that it hits BEFORE it tries to read the Django.core admin code?
I used the Django specified database for admin only, then a redirect gets the 'factory' specified database name. The handler for this redirect manually connects to the correct database. I now have 4 separate remote databases with a local administrative database. Django itself doesn't really handle this internally, so this is a workaround.
def login_view(request):
username = request.POST.get('username', '')
password = request.POST.get('password', '')
factory = request.POST.get('factory', '')
user = auth.authenticate(username=username, password=password)
if user is not None and user.is_active:
# Correct password, and the user is marked "active"
auth.login(request, user)
# Redirect to a success page.
return HttpResponseRedirect("/app/?factory={}".format(factory))
I am using DRF with auth toolkit and it is working fine. However, I want to have a second login api so a user can log in using username and pin number. It is cos we have a USSD application and it is easier to give them a pin based login system.
Currently, I have the following URL that, when called, generates token:
url(r'^signin/', include('oauth2_provider.urls', namespace='oauth2_provider')),
For the ussd app, I want something like that but the auth2 should check pin field, defined in a separate model defined as follows:
class Members(models.Model):
pin=models.IntegerField()
user=models.ForeignKey(User)
I am a little lost where to start.
Using this answer as a base to answer this question, and Django's documentation.
I would say you'd want to create a custom authentication backend, and you'd want a custom user model with two passwords, or using a one-to-one relationship to add the additional password field, something like so:
from django.contrib.auth.models import AbstractBaseUser
class UserExtension(AbstractBaseUser):
user = models.OneToOneField(User)
...
Inheriting from the AbstractBaseUser should add a password field like the user model, (although I haven't tried this). If you prefer the custom user approach, I actually have a github repo that has a custom user app, so if you'd like to get any ideas of how to achieve this check it out.
Or have a look through the documentation.
Either way, once you've got your two passwords, you need to decide which one to use as the pin. If you're using oauth for the pin field and the web applicaiton with the password, I would probably use the standard user password for the pin login, as that way you don't need to change the oauth package to work with your new password. Then for your web application build a custom login. To do this create a custom authentication backend along the lines of:
from django.contrib.auth.models import User
from django.contrib.auth.hashers import check_password
class AuthBackend(object):
supports_object_permissions = True
supports_anonymous_user = False
supports_inactive_user = False
def get_user(self, user_id):
return User.objects.filter(pk=user_id).first()
def authenticate(self, username, password):
user = User.objects.filter(username=username).first()
if not user:
return None
# this is checking the password provided against the secondary password field
return user if check_password(password, user.userextension.password) else None
Then you need to add this authentication backend to your settings:
AUTHENTICATION_BACKENDS = ('myapp.backends.AuthBackend',)
Then create the web application login (as per the stackoverflow answer above):
from django.contrib.auth import authenticate, login
def my_login_view(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
# Redirect to a success page.
else:
# Return a 'disabled account' error message
...
else:
# Return an 'invalid login' error message.
...
You should now have a custom authentication login for the web application using your password2 field, and you can use the oauth authentication to work with the standard Django password in which you're going to store the pin. Which I think is what you're trying to do?
NOTE: All of the above I haven't tested, so this may not work perfectly, but it should hopefully be able to at least point you in the right direction and give you a few ideas. If I'm understanding your problem correctly, this is the sort of approach that I would take to tackle the problem.
I managed to setup allauth to work on my project and use the social media login. However I was wondering if they had any option to set the e-mail fetched Facebook to the username. I read their documentation but I couldn't see any of that in the variables.
I found this link, but they change Django's core files. I was hoping to find something more suitable.
My current configuration:
ACCOUNT_USER_MODEL_EMAIL_FIELD = 'email'
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'email'
SOCIALACCOUNT_QUERY_EMAIL = True
SOCIALACCOUNT_EMAIL_REQUIRED = True
Also, is it possible to use only the social media login/creation creation from Allauth? I can see it is possible to create an account if you access localhost/accounts/create/ (or a similar url). I don't need that since I have my own account creation page.
Well, you don't need to change Django files. Just add this to your models.py:
from django.db.models.signals import pre_save
#receiver(pre_save, sender=User)
def update_username_from_email(sender, instance, **kwargs):
instance.username = instance.email
Assuming an e-mail has been given, it will set the e-mail as the username every time a new user is about to be saved to the database.
So I'll give full disclosure from the get-go that I am quite new to both Django and django-allauth.
Now that that is out of the way, the problem that I am having is that when a user logs in via a social site, (I have been trying Google and Facebook), none of the data retrieved from the site is pulled into the user's data fields. After authenticating, the user is still prompted to enter an email, and all name fields are left blank. I tried to fix this manually by creating a custom adapter, but that did not work either. From using print statements, I can see that the data is being fetched from the site just fine -- it just isn't being saved to the user's attributes.
Correct me if I'm wrong, but by reading the documentation and the some of the source of django-allauth, I am under the impression that social authorization automatically saves the user's email and first and last names via the populate_user(self, request, sociallogin, data): hook in the DefaultSocialAccountAdapter class, so I really shouldn't even have to deal with workarounds.
Thus, I'm guessing that I am just doing something foolish that is messing this up for me... Although if there is a clever workaround that will fix this problem, I'd be fine with that, for lack for a better solution.
Note: Using Django 1.7 and Python 3.4.1
EDIT: Django-allauth is succeeding in creating a User and linking the user to a social account, which contains all of the data fetched from the social site, but none of that data is populating the fields within the User object, like email, first_name, and last_name.
Here are my django-allauth configuration settings in settings.py:
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "required"
ACCOUNT_USERNAME_REQUIRED = False
SOCIALACCOUNT_AUTO_SIGNUP = True
# The following line was uncommented when I was trying to use my own adapter
# SOCIALACCOUNT_ADAPTER = 'profiles.profile_adapter.ProfileAdapter'
SOCIALACCOUNT_PROVIDERS = {
'facebook':
{ 'SCOPE': ['email'],
'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
'METHOD': 'oauth2',
'LOCALE_FUNC': lambda request: 'en_US'},
'google':
{ 'SCOPE': ['https://www.googleapis.com/auth/userinfo.profile'],
'AUTH_PARAMS': { 'access_type': 'online' } },
}
And here is the code I had in my custom adapter (Which, by using print statements, I could tell was getting used and processing the correct data) where I tried to manually save the fields into the user object
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class ProfileAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
'''
Check for extra user data and save the desired fields.
'''
data = sociallogin.account.extra_data
user = sociallogin.account.user
print("LOGS: Caught the signal -> Printing extra data of the account: \n" + str(data))
if 'first_name' in data:
user.first_name = data['first_name']
elif 'given_name' in data:
user.first_name = data['given_name']
if 'last_name' in data:
user.last_name = data['last_name']
elif 'family_name' in data:
user.last_name = data['family_name']
user.save()
Note The above code creates a user in the database that is not linked to any social account, but contains the correct first and last names. Then the user is redirected to a form saying they are logging in with a social account and is prompted for an email address. Once this form is submitted, the original user created is overwritten by a new user that is linked to a social account, contains the email entered into the form, but does not have first or last name fields populated.
The problem was that when an email was not included with the data fetched from the social media site, django-allauth would ask for an email in a subsequent form to create the account with. When the account is then created from this form, django-allauth would not use the data fetched from the social media to populate fields. I think that this is a problem with django-allauth.
So I have a Selenium functional test suite. I've already tested login/signup functionality in a few tests by navigating the Selenium client to the signup page, entering in a username and password, and then telling Selenium to login with those same credentials. Now I want to test other parts of the "login required" areas of the site without having to tell Selenium to click and enter text into the test browser.
In other words, I would like to use something like this (which I use just fine in my view unit tests):
self.client = Client()
self.user = User.objects.create_user('temporary', 'temporary#gmail.com', 'temporary')
self.user.save()
self.client.login(username='temporary', password='temporary')
in my Selenium tests so I don't have to repeat the lengthy manual login process in every one of my tests (since I've already tested the login system in earlier tests as I said before)
As of right now, I just copy and paste the 'login flow' Selenium instructions for each of my tests that require login. This causes my tests to take an addition 5-6 seconds each and it makes my function_tests.py file very bloated.
All my Googling has brought me to pages teaching me how to test login with Selenium.
Thanks in advance.
You can't login user from selenium driver. It's just impossible without some hacks.
But you can login once per TestCase by moving it to setUp method.
You can also avoid copy-pasting by creating your class inherit from LiveServerTestCase.
UPDATE
This code worked for me:
self.client.login(username=superuser.username, password='superpassword') #Native django test client
cookie = self.client.cookies['sessionid']
self.browser.get(self.live_server_url + '/admin/') #selenium will set cookie domain based on current page domain
self.browser.add_cookie({'name': 'sessionid', 'value': cookie.value, 'secure': False, 'path': '/'})
self.browser.refresh() #need to update page for logged in user
self.browser.get(self.live_server_url + '/admin/')
In Django 1.8 it is possible to create a pre-authenticated session cookie and pass it to Selenium.
In order to do this, you'll have to:
Create a new session in your backend;
Generate a cookie with that newly created session data;
Pass that cookie to your Selenium webdriver.
The session and cookie creation logic goes like this:
# create_session_cookie.py
from django.conf import settings
from django.contrib.auth import (
SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY,
get_user_model
)
from django.contrib.sessions.backends.db import SessionStore
def create_session_cookie(username, password):
# First, create a new test user
user = get_user_model()
user.objects.create_user(username=username, password=password)
# Then create the authenticated session using the new user credentials
session = SessionStore()
session[SESSION_KEY] = user.pk
session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
session.save()
# Finally, create the cookie dictionary
cookie = {
'name': settings.SESSION_COOKIE_NAME,
'value': session.session_key,
'secure': False,
'path': '/',
}
return cookie
Now, inside your Selenium tests:
#selenium_tests.py
# assuming self.webdriver is the selenium.webdriver obj.
from create_session_cookie import create_session_cookie
session_cookie = create_session_cookie(
username='test#email.com', password='top_secret'
)
# visit some url in your domain to setup Selenium.
# (404 pages load the quickest)
self.driver.get('your-url' + '/404-non-existent/')
# add the newly created session cookie to selenium webdriver.
self.driver.add_cookie(session_cookie)
# refresh to exchange cookies with the server.
self.driver.refresh()
# This time user should present as logged in.
self.driver.get('your-url')
There is a library available on GitHub for this purpose: django-selenium-login