Manually logging in a user without password
Asked Answered
P

5

81

I hope you can help me figure the best way to implement a manual (server-side initiated) login without using the password. Let me explain the workflow:

  • User registers
  • Thank you! An email with an activation link has been sent blablabla
  • (Account now exists but is marked not enabled)
  • User opens email, clicks link
  • (Account is enabled)
  • Thank you! You can now use the site

What I'm trying to do is log in the user after he has clicked the email link so he can start using the website right away.

I can't use his password since it's encrypted in the DB, is the only option writing a custom authentication backend?

Presidium answered 7/5, 2010 at 9:55 Comment(2)
Please refer the below Thread: #6560682Disorganization
When testing under DRF, see the force_autenticate function and APIClient.force_authenticate method.Serin
I
105

You don't need a password to log a user in. The auth.login function just takes a User object, which you are presumably already getting from the database when you enable the account. So you can pass that straight to login.

Of course, you'll need to be very careful that there's no way a user can spoof a link to an existing already-enabled account, which would then automatically log them in as that user.

from django.contrib.auth import login

def activate_account(request, hash):
    account = get_account_from_hash(hash)
    if not account.is_active:
        account.activate()
        account.save()
        user = account.user
        login(request, user)

... etc.

Edited:

Hmm, didn't notice that requirement to use authenticate because of the extra property it adds. Looking at the code, all it does is a backend attribute equivalent to the module path of the authenticating backend. So you could just fake it - before the login call above, do this:

user.backend = 'django.contrib.auth.backends.ModelBackend'
Intracardiac answered 7/5, 2010 at 10:12 Comment(6)
Thanks; the docs agree, but there's also this warning: "Calling authenticate() first When you're manually logging a user in, you must call authenticate() before you call login(). authenticate() sets an attribute on the User noting which authentication backend successfully authenticated that user (see the backends documentation for details), and this information is needed later during the login process." Could this be a problem?Presidium
Although it's better to import settings from django.contrib.conf and assign settings.AUTHENTICATION_BACKENDS in case use has a custom backend.Pina
this doesn't work, even modified; you have to create an Auth backend and add to your settings; otherwise it will not keep the user logged in between page loads.Pufahl
@Pufahl You are right. I did like Daniel said, but after several ajax requests, django server response to reset cookies session to null, thus user have to logged out. How to solve that?Sec
From Django 1.6 onward it seems to require that the backend setting is actually in the list of AUTHENTICATION_BACKENDS for it to work.Suffrage
From Django 1.10 onwards, this is builtin. See Ian Clark's comment below: https://mcmap.net/q/258462/-manually-logging-in-a-user-without-passwordHesiod
S
30

As of Django 1.10, the process has been simplified.

In all versions of Django, in order for a user to be logged in, they must be authenticated by one of your app's backends (controlled by the AUTHENTICATION_BACKENDS setting).

If you simply want to force a login, you can just claim that the user was authenticated by the first backend from that list:

from django.conf import settings
from django.contrib.auth import login


# Django 1.10+
login(request, user, backend=settings.AUTHENTICATION_BACKENDS[0])

# Django <1.10 -  fake the authenticate() call
user.backend = settings.AUTHENTICATION_BACKENDS[0]
login(request, user)
Stingaree answered 7/9, 2016 at 7:47 Comment(3)
Nice! Small typo. You have settings twice. should be login(request, user, backend=settings.AUTHENTICATION_BACKENDS[0])Hesiod
@Hesiod derp! Thanks very much, fixed!Stingaree
This is a super good answer, working on Django 3.2 in 2021.Ganister
R
27

Daniel's answer is very good.

Another way to do it is to create a HashModelBackend following the Custom Authorization backends https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#writing-an-authentication-backend like this:

class HashModelBackend(object):
    def authenticate(self, hash=None):
        user = get_user_from_hash(hash)
        return user

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

And then install this in your settings:

AUTHENTICATION_BACKENDS = (
    'myproject.backends.HashModelBackend',
    'django.contrib.auth.backends.ModelBackend',
)

Then your view would be something like this:

def activate_account(request, hash):
    user = authenticate(hash=hash)
    if user:
        # check if user is_active, and any other checks
        login(request, user)
    else:
        return user_not_found_bad_hash_message
Rookery answered 7/5, 2010 at 11:12 Comment(3)
It works perfectly. A few comments that could help someone: I use activate_account like a middleware loaded after django's SessionMiddleware and AuthenticationMiddleware. Then I use my cakend before django's ModelBackend. If someone need a full example, please ask and I create a new answer.Tinhorn
@Tinhorn Could you please post your solution. Particularly about why is a Middleware needed here?Romanic
Sorry @Romanic but this comment is about 2 months ago, that code changed on my implementation and I can't remember the example. If I found a commit from 2 months ago I will post the example.Tinhorn
C
3

Response to dan's answer.

A way to write your backend:

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend

class HashModelBackend(ModelBackend):

def authenticate(self, username=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)
        return user
    except UserModel.DoesNotExist:
        return None

Answer is based on django.contrib.auth.backends.ModelBackend source code. It's actual for django 1.9

And I would rather place custom backend below django's default:

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'yours.HashModelBackend',
]

because account activation is less possible than login itself. According to https://docs.djangoproject.com/en/1.9/topics/auth/customizing/#specifying-authentication-backends:

The order of AUTHENTICATION_BACKENDS matters, so if the same username and password is valid in multiple backends, Django will stop processing at the first positive match.

Be careful this code will authenticate your users even with incorrect passwords.

Capsulate answered 26/5, 2016 at 8:13 Comment(1)
There are syntax errors in this code, so it's not clear what's supposed to happen ('hash' is defined but not used, 'username' is used but not defined).Aliquant
P
2

You can use ska package, which has password-less login to Django implemented. ska works with authentication tokens and its security is based on SHARED_KEY which should be equal for all parties (servers) involved.

On client side (party that requests a password-less login), you generate a URL and sign it, using ska. Example:

from ska import sign_url
from ska.contrib.django.ska.settings import SECRET_KEY

server_ska_login_url = 'https://server-url.com/ska/login/'

signed_url = sign_url(
    auth_user='test_ska_user_0',
    secret_key=SECRET_KEY,
    url=server_ska_login_url
    extra={
        'email': '[email protected]',
        'first_name': 'John',
        'last_name': 'Doe',
    }
)

Default lifetime of the token is 600 seconds. You can customise that by proving a lifetime argument.

On the server side (site to which users' log in), having in mind that you have installed ska properly, the user is logged in upon visiting the URL if they existed (username match), or otherwise - created. There are 3 callbacks that you can customise in your project's Django settings.

  • USER_GET_CALLBACK (string): Fired if user was successfully fetched from database (existing user).
  • USER_CREATE_CALLBACK (string): Fired right after user has been created (user didn't exist).
  • USER_INFO_CALLBACK (string): Fired upon successful authentication.

See the documentation (http://pythonhosted.org/ska/) for more.

Phenothiazine answered 21/12, 2013 at 3:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.