I am testing django_python3_ldap library and using the same ldap server that I found in the tutorial (https://ldap3.readthedocs.io/en/latest/tutorial_intro.html), but there is something which I didn't get right to stablish the connection. Does anyone know what is wrong with these settings configuration?
This is the error:
"LDAP bind failed: LDAPInvalidCredentialsResult - 49 - invalidCredentials - None - None - bindResponse - None"
This is my code in settings.py:
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"django_python3_ldap.auth.LDAPBackend",
]
# The URL of the LDAP server.
LDAP_AUTH_URL = "ldap://ipa.demo1.freeipa.org"
# Initiate TLS on connection.
LDAP_AUTH_USE_TLS = False
# The LDAP search base for looking up users.
LDAP_AUTH_SEARCH_BASE = "uid=admin,cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org" #"dc=demo1,dc=freeipa,dc=org"
# The LDAP class that represents a user.
LDAP_AUTH_OBJECT_CLASS = "*"
# User model fields mapped to the LDAP
# attributes that represent them.
LDAP_AUTH_USER_FIELDS = {
"username": "sAMAccountName", #"username",
"first_name": "givenName",
"last_name": "sn",
"email": "mail",
}
# A tuple of django model fields used to uniquely identify a user.
LDAP_AUTH_USER_LOOKUP_FIELDS = ("username",)
# Path to a callable that takes a dict of {model_field_name: value},
# returning a dict of clean model data.
# Use this to customize how data loaded from LDAP is saved to the User model.
LDAP_AUTH_CLEAN_USER_DATA = "django_python3_ldap.utils.clean_user_data"
# Path to a callable that takes a user model, a dict of {ldap_field_name: [value]}
# a LDAP connection object (to allow further lookups), and saves any additional
# user relationships based on the LDAP data.
# Use this to customize how data loaded from LDAP is saved to User model relations.
# For customizing non-related User model fields, use LDAP_AUTH_CLEAN_USER_DATA.
LDAP_AUTH_SYNC_USER_RELATIONS = "django_python3_ldap.utils.sync_user_relations"
# Path to a callable that takes a dict of {ldap_field_name: value},
# returning a list of [ldap_search_filter]. The search filters will then be AND'd
# together when creating the final search filter.
LDAP_AUTH_FORMAT_SEARCH_FILTERS = "django_python3_ldap.utils.format_search_filters"
# Path to a callable that takes a dict of {model_field_name: value}, and returns
# a string of the username to bind to the LDAP server.
# Use this to support different types of LDAP server.
LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_openldap"
# Sets the login domain for Active Directory users.
LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN = ""
# The LDAP username and password of a user for querying the LDAP database for user
# details. If None, then the authenticated user will be used for querying, and
# the `ldap_sync_users` command will perform an anonymous query.
LDAP_AUTH_CONNECTION_USERNAME = "admin"
LDAP_AUTH_CONNECTION_PASSWORD = "Secret123"
ldap3 always authorizes using the displayName field. If the values of your LDAP_AUTH_USER_FIELDS["username"] fields are not equal to the values of the displayName fields, you will have errors.
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))
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.
I have django social-auth installed (from omab) and the users have an email address in database that is the one I want to keep but when the users log in from facebook using social-auth, their email gets replaced by the one they have in their facebook account. I am not sure the settings are the one by default or not and cannot find how to stop this behavior.
Have you tried SOCIAL_AUTH_PROTECTED_USER_FIELDS? :)
From the manual:
The update_user_details pipeline processor will set certain fields on
user objects, such as email. Set this to a list of fields you only
want to set for newly created users:
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email',]
Also more extra values will be stored if defined. Details about this
setting are listed below in the OpenId and OAuth sections.
I found it, in the pipeline the responsible for that is
social_auth.backends.pipeline.user.update_user_details
I just removed it from the pipeline and now the details like email address and name are left to the user to fill.
I'm posting my solution (update user details, not overwrite them) so it may help someone. Based on pipeline.user.update_user_details I coded the following:
def fill_user_details(backend, details, response, user, is_new=False, *args,
**kwargs):
"""Fills user details using data from provider, without overwriting
existing values.
backend: Current social authentication backend
details: User details given by authentication provider
response: ?
user: User ID given by authentication provider
is_new: flag
source: social_auth.backends.pipeline.user.update_user_details
"""
# Each pipeline entry must return a dict or None, any value in the dict
# will be used in the kwargs argument for the next pipeline entry.
#
# If any function returns something else beside a dict or None, the
# workflow will be cut and the value returned immediately, this is useful
# to return HttpReponse instances like HttpResponseRedirect.
changed = False # flag to track changes
for name, value in details.iteritems():
# do not update username, it was already generated
if name in (USERNAME, 'id', 'pk'):
continue
# set it only if the existing value is not set or is an empty string
existing_value = getattr(user, name, None)
if value is not None and (existing_value is None or
not is_valid_string(existing_value)):
setattr(user, name, value)
changed = True
# Fire a pre-update signal sending current backend instance,
# user instance (created or retrieved from database), service
# response and processed details.
#
# Also fire socialauth_registered signal for newly registered
# users.
#
# Signal handlers must return True or False to signal instance
# changes. Send method returns a list of tuples with receiver
# and it's response.
signal_response = lambda (receiver, response): response
signal_kwargs = {'sender': backend.__class__, 'user': user,
'response': response, 'details': details}
changed |= any(filter(signal_response, pre_update.send(**signal_kwargs)))
# Fire socialauth_registered signal on new user registration
if is_new:
changed |= any(filter(signal_response,
socialauth_registered.send(**signal_kwargs)))
if changed:
user.save()
I am having trouble with the LDAP authentification module django-auth-ldap. I am using the example configuration from this site: http://packages.python.org/django-auth-ldap/
I'd like to do two things:
1) Authentificate against LDAP:
For the moment, my LDAP database is empty, I didn't add anything to it, in fact I don't know how to. However, I still am able to log in into my django-based site with my old logins/passwords stored in my django database. Why is that? Shouldn't this be ignored, shouldn't the login process occur with LDAP user/passwords instead? In other words, if my LDAP database is empty, shouldn't every single of my login fail? However, it doesn't, I have the impression that django completly ignores the django-auth-ldap module.
2) Synchronize LDAP with django (and not the other way around)
I don't want to use an existing user database to authentificate against. I want to be able to create new users in Django and propagate these users to LDAP so they can be shared by other services, in my case, an openfire server. How do you do that with django-auth-ldap?
Here is the copy/paste of my configuration:
# Baseline configuration.
AUTH_LDAP_SERVER_URI = "127.0.0.1"
AUTH_LDAP_BIND_DN = "cn=admin,dc=nodomain"
AUTH_LDAP_BIND_PASSWORD = "admin"
AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=nodomain",
ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=django,ou=groups,dc=nodomain",
ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")
# Only users in this group can log in.
AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=nodomain"
# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
AUTH_LDAP_PROFILE_ATTR_MAP = {
"employee_number": "employeeNumber"
}
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=active,ou=django,ou=groups,dc=nodomain",
"is_staff": "cn=staff,ou=django,ou=groups,dc=nodomain",
"is_superuser": "cn=superuser,ou=django,ou=groups,dc=nodomain"
}
AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_FIND_GROUP_PERMS = True
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
# Keep ModelBackend around for per-user permissions and maybe a local
# superuser.
AUTHENTICATION_BACKENDS = (
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
)
Sorry I don't know much about LDAP, I just installed it this morning so my question may sound naive. I just need a centralized user base that I would be able to update and share between several servers.
Thanks very much for your help.
1) Your configuration has two authentication backends installed:
AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', )
Django will attempt to authenticate against each one in turn until it finds one that succeeds (or until it runs out). Since your LDAP directory is empty, it will presumably always fail, so ModelBackend will always get a shot. If you don't want to authenticate users against the Django user database, you have to remove ModelBackend from the list.
2) django-auth-ldap doesn't propagate Django users up to LDAP, only the other way around. It's designed to allow Django deployments to authenticate against existing LDAP services that are managed separately. To manipulate the contents of an LDAP directory from a Django app you might want to look at django-ldapdb.
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 :)