How to implement ldap authentication(without admin credentials) in django - django

My setup: Django-3.0, Python-3.8, django_auth_ldap
I have LDAP Server (Active Directory Server) in my organization.
I am building a Django Application which serves some operations for all the users.
I know Django has built-in User Authentication mechanism, But it authenticates if users are present in User Model Database.
But my requirement is.
All user entries are in LDAP Server(Active Directory). Using proper user credentials LDAP server authenticates me.
I Created a Login Page in Django 'accounts' app,
1. whenever I enter username and password from Login Page, it should Authenticate using My Organization LDAP Server.
2. After Login I have to hold the session for the logged in user for 5 active minutes. (Django auth session)
I saw django_auth_ldap package gives some insight for my purpose.
I have these contents in settings.py.
import ldap
##Ldap settings
AUTH_LDAP_SERVER_URI = "ldap://myldapserver.com"
AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS : 0}
AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s, OU=USERS,dc=myldapserver, dc=com"
AUTH_LDAP_START_TLS = True
#Register authentication backend
AUTHENTICATION_BACKENDS = [
"django_auth_ldap.backend.LDAPBackend",
]
Calling authenticate in views.py.
from django_auth_ldap.backend import LDAPBackend
def accounts_login(request):
username = ""
password = ""
if request.method == "POST":
username = request.POST.get('username')
password = request.POST.get('password')
auth = LDAPBackend()
user = auth.authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return redirect("/")
else:
error = "Authentication Failed"
return render(request, "accounts/login.html", 'error':error)
return render(request, "accounts/login.html")
But using above method always authenticate fails with the LDAP Server.
If I call using normal python simple_bind_s(), authentication is working fine to same LDAP server.
import ldap
def ldap_auth(username, password):
conn = ldap.initialize(myproj.settings.LDAP_AUTH_URI)
try:
ldap.set_option(ldap.OPT_REFERRALS, 0)
#ldap.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
conn.simple_bind_s(username, password)
except ldap.LDAPError as e:
return f'failed to authenticate'
conn.unbind_s()
return "Success"
Can anybody suggest me to make LDAPBackend Authentication work as per my requirement ?
Note: I do not have admin permission of LDAP Server.

This is how I would do it with ldap3 and without django_auth_ldap packages.
1 - Create a custom AuthenticationBackend in your_app/backends.py :
import logging
from ldap3 import Server, Connection
from ldap3.core.exceptions import LDAPBindError
from django.conf import settings
from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__)
UserModel = get_user_model()
class LDAPBackend:
def authenticate(self, request, username=None, password=None, **kwargs):
# set username to lowercase for consistency
username = username.lower()
# get the bind client to resolve DN
logger.info('authenticating %s' % username)
# set your server
server = Server(settings.LDAP_HOST, get_info=ALL)
try:
conn = Connection(server, f"{username}#{settings.LDAP_DOMAIN}", password=password, auto_bind=True)
except LDAPBindError as e:
logger.info('LDAP authentication failed')
logger.info(e)
return None
user = UserModel.objects.update_or_create(username=username)
return user
def get_user(self, user_id):
try:
return UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
2 - Declare LDAPBackend as your authentication backend in settings.py
AUTHENTICATION_BACKENDS = [
'your_app.backends.LDAPBackend',
'django.contrib.auth.backends.ModelBackend'
]
This allows you to use django's native function for authentication, and it work with the admin.
To have session only work for 5 minutes, add this setting to your settings.py file :
SESSION_COOKIE_AGE = 5 * 60
Let me know if it works for your.

Related

Django testing how to make a request as logged in user?

In Django tests, I want to log in and make a request.
Here the code
def test_something(self):
self.client.login(email=EMAIL, password=PASSWORD)
response = self.client.get(reverse_lazy("my_releases"))
self.assertEqual(response, 200)
But the code doesn't work and returns
AttributeError: 'AnonymousUser' object has no attribute 'profile'
How to make request in test as logged in user?
As the documentation on .login(…) says:
(…)
login() returns True if it the credentials were accepted and login
was successful.
Finally, you’ll need to remember to create user accounts before
you can use this method.
The tests run normally on a separate database, and thus the items created while debugging, or in production, do not have effect on that.
You thus first create a user and then test it with:
from django.contrib.auth import get_user_model
# …
def test_something(self):
User = get_user_model()
User.objects.create_user('my-user-name', email=EMAIL, password=PASSWORD)
self.assertTrue(self.client.login(email=EMAIL, password=PASSWORD))
response = self.client.get(reverse_lazy("my_releases"))
self.assertEqual(response, 200)
The standard authentication engine works with a username and password, not with an email and password, so if you used the builtin authentication manager, you login with:
from django.contrib.auth import get_user_model
# …
def test_something(self):
User = get_user_model()
User.objects.create_user('my-user-name', email=EMAIL, password=PASSWORD)
self.assertTrue(self.client.login(username='my-user-name', password=PASSWORD))
response = self.client.get(reverse_lazy("my_releases"))
self.assertEqual(response, 200)

Custom Authentication Backend in Django

How to write a custom authentication backend in Django taking scenario as Phone Number & OTP(One-Time Password) to authenticate against each user.
How to authenticate each user in form of multiple conditions.
If email is verified and password exist ( authenticate using email and password).
If phone is verified and exist( authenticate using phone and otp or if password exist then auth using phone and password).
from django.contrib.auth import backends, get_user_model
from django.db.models import Q
class AuthenticationBackend(backends.ModelBackend):
"""
Custom authentication Backend for login using email,phone,username
with password
"""
def authenticate(self, username=None, password=None, **kwargs):
usermodel = get_user_model()
try:
user = usermodel.objects.get(
Q(username__iexact=username) | Q(email__iexact=username) | Q(phone__iexact=username)
if user.check_password(password):
return user
except usermodel.DoesNotExist:
pass
For you have to specify the authclass in settings.py
AUTHENTICATION_BACKENDS = (
'applications.accounts.auth_backends.AuthenticationBackend',
)
There are many ways to extend user model, here I leave you this page and you can choose which of them is better for you https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html

Django (using TokenAuthentication): "non_field_errors": "Unable to log in with provided credentials?

I'm using httpie to test my custom authentication.
http POST http://127.0.0.1:8000/api-token-auth/ username='username1' password='Password123'
I did create a custom auth object using AbstractUser.
Using TokenAuthentication, I followed the docs and added my custom TokenAuthentication in my REST_FRAMEWORK settings:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'regis.models.CustomAuthentication',
)
}
And added rest_framework.authtoken in my installed apps.
My AUTHENTICATION_BACKEND is as follows:
AUTHENTICATION_BACKENDS = [ 'regis.models.CustomAuthentication' ]
And here is my custom authentication class:
class CustomAuthentication(authentication.TokenAuthentication):
def authenticate(self, request):
username = request.META.get('X_USERNAME')
print(username)
user_model = get_user_model()
if not username:
return None
try:
user = user_model.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
urls.py:
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token),
]
I'm pretty much following the DRF docs (http://www.django-rest-framework.org/api-guide/authentication/#custom-authentication). If there's any additional info needed to solve this, please let me know and I'll update. Any help on what I'm missing would be great.
To add: Just out of curiosity, do I need to make a custom authentication system if I have a custom user?
UPDATE:
I just deleted the class above, and just added the rest_framework.authentication.TokenAuthentication in my REST_FRAMEWORK settings. I'm still using a custom authentication which fetches my user.
It looks like this (not going to format it. SO sucks at formatting code from VIM):
class CustomAuthentication(object):
def authenticate(self, email=None, password=None):
User = get_user_model()
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return None
if user.check_password(password):
return user
return None
def get_user(self, user_id):
try:
user_model = get_user_model()
user = user_model.objects.get(pk=user_id)
except User.DoesNotExist:
return None
I used this Django docs to create that code: https://docs.djangoproject.com/en/1.10/topics/auth/customizing/
If you search for the error string in the DRF code, you find this (in authtoken/serializers.py:
from django.contrib.auth import authenticate
...
if username and password:
user = authenticate(username=username, password=password)
if user:
# From Django 1.10 onwards the `authenticate` call simply
# returns `None` for is_active=False users.
# (Assuming the default `ModelBackend` authentication backend.)
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg, code='authorization')
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg, code='authorization')
...
So it looks like depending on which version of Django you're using, either these credentials are incorrect, or the user is not active (for Django >= 1.10)?
Have you tried logging in manually in the admin with these credentials to verify them?
OK I solved it. Inside my settings, I just had to remove the AUTHENTICATIONS_BACKEND. I thought my custom backend was different for merely logging a user in and the token authentication backend worked to get that token.

How to use django.contrib.auth with django-rest-framework?

django-rest-framework makes use of django.contrib.auth for authentication and authorization (as stated in the django-rest-framework authentication api guide)
However, no-where in the documentation does it talk about how users are actually authenticated using the rest-framework
By default the django.contrib.auth views will respond with a server-side rendered login form.
However, if using a client-side framework such as AngularJs this is not desired - you simply want an api endpoint against which you can authenticate.
Questions:
Is there django-rest-framework documentation I am somehow missing which explains how user authentication is done-out-of-the-box?
Does an out-of-the-box solution even exist?
If not, what is the recommended way of achieving this with minimal reinvention of the wheel?
lets say that you have login view:
Note: with this method you have to assure SSL/TLS because username and password are sending as plain text.
import json
import requests
def login(request):
if request.method == "POST":
username = request.POST['username']
password = request.POST['password']
login_url = 'http://your_url:port/rest-api/login/'
response = requests.post(login_url, data={'username': username, 'password': password})
response = json.loads(response.text)
if response.status_code == 200:
return render_to_response("login.html", {"success": True}, RequestContext(request))
your view in rest-api:
from django.contrib.auth.backends import ModelBackend as DjangoModelBackend
def login(request):
response = base_response.copy()
username = request.DATA.get('username', '')
password = request.DATA.get('password', '')
user = DjangoModelBackend().authenticate(username=email, password=password)
if user is not None:
response["message"] = "Authenticated"
else:
response["message"] = "Login Failed"
return Response(response)
and here is the part of ModelBackend
from django.contrib.auth import get_user_model
class ModelBackend(object):
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
if user.check_password(password):
return user
except UserModel.DoesNotExist:
return None
You don't usually go through login forms when authenticating yourself at an API endpoint - you either use an API token or send the authentication credentials through a header, see How to use Basic Auth with jQuery and AJAX? on how to do that.

django authentication without a password

I'm using the default authentication system with django, but I've added on an OpenID library, where I can authenticate users via OpenID. What I'd like to do is log them in, but it seems using the default django auth system, I need their password to authenticate the user. Is there a way to get around this without actually using their password?
I'd like to do something like this...
user = ... # queried the user based on the OpenID response
user = authenticate(user) # function actually requires a username and password
login(user)
I sooner just leave off the authenticate function, but it attaches a backend field, which is required by login.
It's straightforward to write a custom authentication backend for this. If you create yourapp/auth_backend.py with the following contents:
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class PasswordlessAuthBackend(ModelBackend):
"""Log in to Django without providing a password.
"""
def authenticate(self, username=None):
try:
return User.objects.get(username=username)
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Then add to your settings.py:
AUTHENTICATION_BACKENDS = (
# ... your other backends
'yourapp.auth_backend.PasswordlessAuthBackend',
)
In your view, you can now call authenticate without a password:
user = authenticate(username=user.username)
login(request, user)
This is a bit of a hack but if you don't want to rewrite a bunch of stuff remove the authenticate
user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, user)
user would be your User object
In order to do authenticate without password, in your settings.py:
AUTHENTICATION_BACKENDS = [
# auth_backend.py implementing Class YourAuth inside yourapp folder
'yourapp.auth_backend.YourAuth',
# Default authentication of Django
'django.contrib.auth.backends.ModelBackend',
]
In your auth_backend.py:
NOTE: If you have custom model for your app then import from .models CustomUser
from .models import User
from django.conf import settings
# requires to define two functions authenticate and get_user
class YourAuth:
def authenticate(self, request, username=None):
try:
user = User.objects.get(username=username)
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
In your Views for custom login request:
# Your Logic to login user
userName = authenticate(request, username=uid)
login(request, userName)
For further reference, use the django documentation here.
You can easily fix this by creating your own authentication backend and adding it to the AUTHENTICATION_BACKENDS setting.
There are some OpenID backends available already, so with a bit of searching you could save yourself the trouble of writing one.