Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoEngine -- how to custom User model / custom backend for authenticate()

SUMMARY

How do I use a custom User model and a custom authentication backend (to allow for email / password authentication) with Django + MongoEngine? (Is a custom backend even necessary for that? ...i.e., to use an email for username when authenticating with MongoEngine.)

Is there any documentation with a straight-forward (and complete!) example of using a custom user object while using Mongo as the primary datastore when authenticating in Django? (Postgres has such clearer and more comprehensive docs...)


DETAIL

MongoEngine seems to give you just two flavors of authentication--the "Classic" (aka 'mongoengine.django.auth.MongoEngineBackend') way...OR...the "Custom User Model" (aka 'django.contrib.auth.backends.ModelBackend') way--both of which more or less succinctly outlined in Nicolas Cortot's answer to a different question here:

Python-Social-Auth fails with mongoEngine (Django)

Both of these authentication techniques give you access to an authenticate() method similar to Django's AbstractBaseUser class--a method which relies on a check_password function. However, the minute you use the so-called "Custom User Model" flavor of authentication (as outlined in the above link)...and then pair that with a custom backend (in order to use emails for usernames)...you run into trouble due to the absence of access to the typical authenticate() function.

For example, like so...

accounts.models.py


# ...with postgres, I'd subclass AbstractBaseUser...but with Mongo...(?)

from django.conf import settings
from mongoengine.fields import EmailField, BooleanField 
from mongoengine.django.auth import User class MyUser(User): email = EmailField(max_length=254, unique=True) is_active = BooleanField(default=True) is_admin = BooleanField(default=False) USERNAME_FIELD = 'email' REQUIRED_FIELDS = '' ...

my_custom_backend.py

# ...is a custom backend even necessary to use email for authentication instead of username?

from django.conf import settings
from django.contrib.auth.models import check_password
#from mongoengine.django.auth import check_password
#from django.contrib.auth.hashers import check_password
from models import MyUser

    class EmailAuthBackend(object):

        def authenticate(self, email=None, password=None):

# ...uh oh, since I'm NOT using one of the usual backends with a pre-existing authenticate()
# method, there ain't a native check_password() function available. Means I have to hash the
# password, etc.

So, seemingly, I'm obliged to write my own check_password function. To get all of the goodness inherent with the AbstractBaseUser class typically found with a PostgreSQL authentication, I'd have to totally inflate my custom User model, which seems hacky and can't be very DRY.

Am I getting totally confused here? ...i.e., is it actually totally unnecessary to use a custom backend if I want to use emails instead of usernames for authentication when using MongoEngine?

I feel like I may have a fundamental misunderstanding of how Django works with MongoEngine in regard to authentication, and with regard to how I've modeled and called upon custom user object / my particular subclassing of MongoEngine's user object during that process...

Because--as it is right now--I'm getting an "'AnonymousUser' object has no attribute 'backend'" error message in the browser. I also noted that this problem sometimes exists for unexpected reasons--namely: perhaps, the authenticate() method expects a hashed password, or because the login (email) is too long...? For more instances where this latter circumstance might be the case, see:

Django Register Form 'AnonymousUser' object has no attribute 'backend'


settings.py

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.admin',
    'mongoengine.django.mongo_auth',
    'accounts',
)

AUTHENTICATION_BACKENDS = (
    'mongoengine.django.auth.MongoEngineBackend',
    #'accounts.my_custom_backend.EmailAuthBackend',
    #'django.contrib.auth.backends.ModelBackend',
)

AUTH_USER_MODEL = 'mongo_auth.MongoUser'
MONGOENGINE_USER_DOCUMENT = 'accounts.models.User'

accounts.views.py

from django.contrib.auth import login as django_login
from my_custom_backend import EmailAuthBackend
from forms import AuthenticationForm

def login(request):

    form = AuthenticationForm(data=request.POST)
    if form.is_valid():
        try:
            backend = EmailAuthBackend()
            user = backend.authenticate(email=request.POST['email'], password=request.POST['password'])
            django_login(request, user)
            return redirect('/')
        except DoesNotExist:
            return HttpResponse('user does not exist')
    else:
        form = AuthenticationForm()

    return render_to_response('accounts/login.html',
       { 'form': form },
       context_instance=RequestContext(request))
like image 255
Sean Avatar asked Jan 12 '14 08:01

Sean


2 Answers

Well, looks like the best course of action isn't to hand over Django's User to Mongo for authentication to begin with... Got this golden nugget via Twitter:

@blogblimp my short answer: try to avoid replacing Django user models with MongoDB. You lose all the Django power and lose MongoDB's speed. Seriously, user relates to everything and MongoDB isn't relational.

— Daniel Roy Greenfeld (@pydanny) January 20, 2014


So: I'll just leverage PostgreSQL for authentication, and Mongo for other objects. That means naming / connecting to two databases in the Django settings. In retrospect, I guess the moral is: never use Mongo just because it's cool. Mongo is still a second-class citizen in the Django world.

like image 61
Sean Avatar answered Oct 16 '22 20:10

Sean


Maybe I'm a little late, but I could achieve the task of email authentication using mongoengine User + django authenticate, here is how I'm working:

from django.contrib.auth import authenticate, login as do_login, logout as do_logout

def login(request):

    data = extractDataFromPost(request)
    email = data["email"]
    password = data["password"]

    try: 
        user = User.objects.get(username=email)
        if user.check_password(password):
            user.backend = 'mongoengine.django.auth.MongoEngineBackend'
            user = authenticate(username=email, password=password)
            do_login(request, user)
            request.session.set_expiry(3600000) # 1 hour timeout
            return jsonResponse(serializeUser(user)) 
        else:
            result = {'error':True, 'message':'Invalid credentials'}
            return jsonResponse(result) 
    except User.DoesNotExist:
        result = {'error':True, 'message':'Invalid credentials'}
        return jsonResponse(result)
like image 27
André Teixeira Avatar answered Oct 16 '22 19:10

André Teixeira