I'm using Django 1.8.4 on Python 3, and attempting to create an auth backend which validates a cookie from a legacy ColdFusion web site and create / log the Django user in after checking the value in a database. In settings, I am including the backend:
AUTHENTICATION_BACKENDS = (
'site_classroom.cf_auth_backend.ColdFusionBackend',
)
And the code for the backend itself; SiteCFUser is a model against the SQL Server database user model which contains the active cookie token value:
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from users.models import SiteCFUser
class ColdFusionBackend(ModelBackend):
"""
Authenticates and logs in a Django user if they have a valid ColdFusion created cookie.
ColdFusion sets a cookie called "site_web_auth"
Example cookie: authenticated#site+username+domain+8E375588B1AAA9A13BE03E401A02BC46
We verify this cookie in the MS SQL database 'site', table site_users, column user_last_cookie_token
"""
def authenticate(self, request):
User = get_user_model()
print('Hello!')
token=request.COOKIES.get('site_web_auth', None)
print('Token: ' + token)
cookie_bites = token.split('+')
if cookie_bites[0] != "authenticated#site":
# Reality check: not a valid site auth cookie
return None
username = cookie_bites[1]
cf_token = cookie_bites[3]
try:
site_user = SiteCFUser.objects.using('mssqlsite').filter(cf_username=username)
except:
# No user found; redirect to login page
return None
if site_user[0].cftoken == cf_token:
try:
# Does the user exist in Django?
user = User.objects.get(username=username)
except:
# User does not exist, has a valid cookie, create the User.
user = User(username=username)
user.first_name = site_user[0].cf_first_name
user.last_name = site_user[0].cf_last_name
user.email = site_user[0].cf_email
user.save()
else:
return None
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
The problem is, the backend doesn't seem to be called when hitting a URL with a view with #login_required, or even trying to log in through a form with username and password. If I force an error by changing the name of the class in settings, or change the name of the class in cf_auth_backend.py, I do get an error. However, none of the print statements show up in the console. I'm clearly missing something here: any idea what I'm not doing right?
While the accepted answer might have helped the OP, it's not a general answer to the question's title.
Authentication back ends do work simply by listing them in AUTHENTICATION_BACKENDS. But they may appear to be ignored
for various reasons, e.g.:
urls.py needs to point to something like django.contrib.auth.views.login
url(r'^accounts/login/$', django.contrib.auth.views.login)
if it's pointing to some other authentication app. AUTHENTICATION_BACKENDS
may not work.
the authenticate() method must accept a password keyword, either through
password=None or **kwargs. Probably true for username too. It won't
be called if it doesn't accept that keyword argument.
Authentication backends doesn't work that way. They won't be called on each request or on requests where authentication is required.
If you want to log in user based on some cookie, you should call authentication in middleware.
Related
I've overridden the ModelBackend in my django app. My overridden model backend requires that headers be present in the request to log in the user.
HEADER = 'testing'
class TestingModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
testing_header_value = None
if request is not None and request.META is not None:
testing_header_value = request.META.get(HEADER, None)
if username is None:
username = kwargs.get(User.USERNAME_FIELD)
try:
user = User.objects.get_by_natural_key(username, testing_header_value)
except User.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
User().set_password(password)
else:
# now validate password and whether the user is active
if user.check_password(password) and self.user_can_authenticate(user):
return user
This works perfectly in non test scenarios. However, when I test I'm running into a problem of passing headers with the test client.
The Django test client has a login method but it doesn't pass the request when authenticating which means that my model backend can't function correctly - I can't pass the header I need to. Note that one of the parameters in the authenticate function is the current request.
I see that I can use force_login but that seems a little hack-y. What is the correct way to do this? I suspect subclassing the default test client and overriding the login method might be best but I'm not sure.
I believe force_login() is the best thing to use in your case.
I am kind of new to django and I am concerned about can we use multiple models for authentication for different types of users for our application e.g for Customers use Customer model, for Suppliers use Supplier model and also keep default User registration model for administration use only? If so can you point me in the right direction, how it can be done? Which package should be used?
There is one way that I came around is by adding a foreign key to each model viz needed for authentication but that will involve joins in every query which could result in performance issues. I need to know if there is a better way. An also these custom models can benefit for all permissions stuff available in admin panel.
Expert opinion will be really appreciated.
There are a few different options to handle that.
Maybe check first the Django-docu.
I've you'ld like to customize it to authenticate your users with a mail-address, here is an example written by knbk and rahul-gupta on the ex-documentation:
Django's default authentication works on username and password fields. Email authentication backend will authenticate users based on email and password.
from django.contrib.auth import get_user_model
class EmailBackend(object):
"""
Custom Email Backend to perform authentication via email
"""
def authenticate(self, username=None, password=None):
user_model = get_user_model()
try:
user = user_model.objects.get(email=username)
if user.check_password(password): # check valid password
return user # return user to be authenticated
except user_model.DoesNotExist: # no matching user exists
return None
def get_user(self, user_id):
user_model = get_user_model()
try:
return user_model.objects.get(pk=user_id)
except user_model.DoesNotExist:
return None
Add this authentication backend to the AUTHENTICATION_BACKENDS setting.
# settings.py
AUTHENTICATION_BACKENDS = (
'my_app.backends.EmailBackend',
...
)
I am using the OAUTH API to verify access to the users google calendar. They are not logging into my system using OAUTH, just accepting my site accessing their data.
The problem is, if the user is logged out of their google account and after they hit verify, it force logs them out of their session and I have no way of linking them back up.
This DOES work if they are already logged into the google account in their browser session and hit accept, they will be redirected to the right page.
I replicate the error when the cache, cookies are clear and they need to relogin into their google account to verify.
I've tried storing the session ID, etc, but the request parameter is not containing the same request data as the initial view, so there is a conflict in the data I am trying to retrieve.
The user is logged in using the standard Django libraries for the credentials model.
CODE
FLOW = flow_from_clientsecrets(
CLIENT_SECRETS,
scope='https://www.googleapis.com/auth/calendar.readonly',
redirect_uri='http://127.0.0.1:8000/oauth2callback')
'''''''''''''''''''''''''''''''''''''''''''''''''''
Main function dealing with auth verification
'''''''''''''''''''''''''''''''''''''''''''''''''''
def index(request):
current_user = User.objects.get(username=request.user.username)
storage = Storage(CredentialsModel, 'id', current_user, 'credential')
credential = storage.get()
if credential is None or credential.invalid == True:
FLOW.params['state'] = xsrfutil.generate_token(settings.SECRET_KEY,
request.user.id)
authorize_url = FLOW.step1_get_authorize_url()
return redirect(authorize_url)
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
User then calls the data function once authenticated
''''''''''''''''''''''''''''''''''''''
def auth_return(request):
print("THE CURRENTLY REQUESTED USER IN THIS SESSION REQUEST IS %s"%(request.user.username))
credential = FLOW.step2_exchange(request.REQUEST)
try:
current_user = User.objects.get(id=request.user.id)
except:
return HttpResponseRedirect("/login")
storage = Storage(CredentialsModel, 'id', current_user, 'credential')
storage.put(credential)
return HttpResponseRedirect("/get_cal")
Ok, so this was a bit more involved than I thought.
I fixed this by adding a state parameter of the currently logged in username.
logged_in_username = request.user.username
user_data = {'var_test' : logged_in_username}
pass_param = urllib.urlencode(user_data)
FLOW.params['state']=pass_param
authorize_url = FLOW.step1_get_authorize_url()
This gave me the ability to query the user from the DB via the User model in Django contrib. I parsed out the state var from the URL:
#Get the currently logged in username
user_variable_data = str(FLOW.params['state'])
#get rid of the var_test= preprended text data for parsing reasons
user_variable_data = user_variable_data[9:]
#Get that user from the database in the form of a user object
current_user = User.objects.get(username=user_variable_data)
and then built a custom backend authentication file to auth the user without a password to maintain the request like nothing weird ever even happened.
user = authenticate(username=user_variable_data)
login(request, user)
print("AUTHENTICATED")
Appended this to settings file
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'procrastinate.auth_backend.PasswordlessAuthBackend',
)
Custom Backend File
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
I implemented my own User class from scratch in Django. But when I log in I have this error:
The following fields do not exist in this model or are m2m fields: last_login
I really don't want the field last_login.
I do some reasearch and the problem is here: contrib.aut.models.py
def update_last_login(sender, user, **kwargs):
"""
A signal receiver which updates the last_login date for
the user logging in.
"""
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
user_logged_in.connect(update_last_login)
I found a workaround but it's not an ellegant solution. I added user_logged_in.disconnect(update_last_login) in my models.py file, where my User class is defined.
Is there any better solution for this?
Not sure if this is related to a newer version of django or what, but in my case
user_logged_in.disconnect(update_last_login)
didn't work. This is what works for me (django 2.1):
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
Currently in Django 1.7...
I think the workaround you defined is the only valid solution (besides from a monkey patch) currently when using the Django auth login() method. I'm just going to assume you are using the standard login() method which is raising this exception.
If we take a look at the source for the login method, we find at the end of the method, a call to execute user_logged_in.send(sender=user.__class__, request=request, user=user). We can't prevent this signal from executing besides from disconnecting it as you have pointed out.
Alternatively, we could monkey patch the login() method to remove that signal call.
from django.contrib.auth import login
def monkey_patch_login(request, user):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
session_auth_hash = ''
if user is None:
user = request.user
if hasattr(user, 'get_session_auth_hash'):
session_auth_hash = user.get_session_auth_hash()
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash and
request.session.get(HASH_SESSION_KEY) != session_auth_hash):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
request.session[BACKEND_SESSION_KEY] = user.backend
request.session[HASH_SESSION_KEY] = session_auth_hash
if hasattr(request, 'user'):
request.user = user
rotate_token(request)
login = monkey_patch_login
We would put the monkey patch code at the top of the file that needs to call the login() method.
I have an application written in Django and I have to extend it and include some other solution as an "app" in this application.
For example, my app to be integrated is named "my_new_app"
Now there is a backend authentication written for the main application and I cannot use it.
I have a MySQL DB to query from and the main app uses Cassandra and Redis mostly.
Is there any way I can use a separate authentication backend for the new app "my_new_app" and run both in the same domain?
You can have multiple authentication backends. Just set the AUTHENTICATION_BACKENDS in settings.py of your Django project to list the backend implementations you want to use. For example I often use a combination of OpenID authentication and the standard Django authentication, like this in my settings.py:
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'django_openid_auth.auth.OpenIDBackend',
)
In this example Django will first try to authenticate using django.contrib.auth.backends.ModelBackend, which is the default backend of Django. If that fails, then it moves on to the next backend, django_openid_auth.auth.OpenIDBackend.
Note that your custom backends must be at a path visible by Django. In this example I have to add django_openid_auth to INSTALLED_APPS, otherwise Django won't be able to import it and use it as a backend.
Also read the relevant documentation, it's very nicely written, easy to understand:
https://docs.djangoproject.com/en/dev/topics/auth/customizing/
I've been through this problem before. This is the code I used.
This is the authentication backend at the api/backend.py
from django.contrib.auth.models import User
class EmailOrUsernameModelBackend(object):
def authenticate(self, username=None, password=None):
if '#' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = User.objects.get(**kwargs)
if user.check_password(password):
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
And this is my settings.py
AUTHENTICATION_BACKENDS = (
'api.backend.EmailOrUsernameModelBackend',
'django.contrib.auth.backends.ModelBackend',
)
This code will enable you to use email to authenticate the default Django user even in Django admin.
Using multiple backend authentications is as simple as pie. You just need to understand the workflow of Django apps.
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.Backend1',
'django_openid_auth.auth.Backend2',
)
For example you have the following two backends defined. Django will first go to the first backend and you just need to put some logic in that backend so that, if its not related to that backend it get forwarded to the other backend or returned without any results. In case of no results django will automatically shift the request from first backend to the second and if available third one.
I spend a lot of time on this and found out that it was not that complex.
Using Multiple AUTHENTICATION BACKENDS is very easy,
you just need to add this to settings.py
AUTHENTICATION_BACKENDS = (
'social_core.backends.open_id.OpenIdAuth',
'social_core.backends.google.GoogleOpenId',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.google.GoogleOAuth',
'social_core.backends.facebook.FacebookOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
and this may create a problem at signup page so add a login argument in your signup view in views.py file like this
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
def signup_view(request):
if request.method=='POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user=form.save()
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
return redirect('home')
Done a little safety and reassemble for code. cause provided solutions wasn't perfectly matching necessary.
my version for the previous: https://stackoverflow.com/a/17078818/14972653
with my addition and #M.Void comment:
This is the authentication backend at the api/backend.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.validators import validate_email
user_model = get_user_model()
class EmailOrUsernameModelBackend(ModelBackend):
"""EmailOrUsernameModelBackend - UsernameOrEmail custom authentication backend
with email or username validation on same field
provides case insensitive validation of username and email
BEWARE - if you allow users to register two usernames in differren cases like: Alex and alex - there would be error.
still uses ModelBackend so provides permissions and is_active validation
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(user_model.USERNAME_FIELD)
if username is None or password is None:
return
try:
validate_email(username)
except ValidationError as e:
kwargs = {'username__iexact': username} # remove __iexact to make it case sensitive
else:
kwargs = {'email__iexact': username} # remove __iexact to make it case sensitive
try:
user = user_model.objects.get(**kwargs)
except user_model.DoesNotExist:
return None
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
def get_user(self, user_id):
try:
return user_model.objects.get(pk=user_id)
except user_model.DoesNotExist:
return None
and of course add it to your authentication backends:
AUTHENTICATION_BACKENDS = (
'api.backend.EmailOrUsernameModelBackend',
'django.contrib.auth.backends.ModelBackend',
)