Authentication backend - serialization

103 views
Skip to first unread message

Alison P

unread,
Jun 5, 2017, 7:33:28 AM6/5/17
to Django users
Hi everyone,

I have written a custom authentication backend, the code is below. It allows a user to click "email me a one-time password" on the home page, which is saved on the "Person" model (which extends User through a foreign key) and then log in with that password. This backend verifies the password and then erases it from the database.
This whole thing works when I put SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer' in settings.py, but I don't want that since PickleSerializer is unsafe.

If I use the default session serializer, I get the following error:

TypeError at /login/

<class 'OneTimePasswordBackend'> is not JSON serializable

how do I solve this? Do I need to write a custom serializer, and if yes, how? Can I add serialize/deserialize methods on this class, and what exactly do they need to do? Do they need to be classmethods or something?

I would really appreciate some help with this. Thanks in advance!

from django.contrib.auth.models import User
from allauth.account.models import EmailAddress
from passlib.hash import pbkdf2_sha256
from api import models
from base.settings import ACCOUNT_PASSWORD_MIN_LENGTH

class OneTimePasswordBackend(object):
    def authenticate(self, email=None, one_time_password=None):
        if len(one_time_password) < ACCOUNT_PASSWORD_MIN_LENGTH or one_time_password==None:
            return None
        try:
            email_obj = EmailAddress.objects.get(email=email)
        except EmailAddress.DoesNotExist:
            return None
        user = email_obj.user
        person = models.Person.objects.get(user_account=user)
        saved_pw = person.one_time_password
        try:
            verify = pbkdf2_sha256.verify(one_time_password, saved_pw)
        except Exception as e:
            print(e)
            verify = False
        else:
            """reset the one time password"""
            person.one_time_password = ""
            person.save()
            return user
        return None

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

ludovic coues

unread,
Jun 5, 2017, 3:06:40 PM6/5/17
to django...@googlegroups.com
I don't see where in your code the error happen.

With standard python, you can't add a method on your class to make is
JSON serializable. At least to my knowledge. You have to write your
own serializer and instruct the code turning your object into JSON to
use it.

But there are a few easier alternative. For example, model_to_dict (
https://docs.djangoproject.com/en/1.11/_modules/django/forms/models/
). This function will take your model, a list of field then build a
dict. As long as you are not using fancy fields like a DateTimeField,
the dict should be JSON serializable.
> --
> You received this message because you are subscribed to the Google Groups
> "Django users" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to django-users...@googlegroups.com.
> To post to this group, send email to django...@googlegroups.com.
> Visit this group at https://groups.google.com/group/django-users.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/django-users/69b91fa4-aca9-458e-9a83-d7b3d3ac35f7%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.



--

Cordialement, Ludovic Coues
+33 6 14 87 43 42

Melvyn Sopacua

unread,
Jun 5, 2017, 6:34:47 PM6/5/17
to django...@googlegroups.com

On Monday 05 June 2017 04:14:01 Alison P wrote:

 

> If I use the default session serializer, I get the following error:

> TypeError at /login/

>

> <class 'OneTimePasswordBackend'> is not JSON serializable

 

I'm using this is a general solution. Feel free to strike what you don't need.

The basics is that a JSONEncoder knows primitives and how to recurse containers. The rest is passed to a method "default", which generates the above error message. Subclasses should override it to implement their own knowledge about objects to serialize.

 

class JSONModelEncoder(DjangoJSONEncoder):
exclude_callback = None
recurse_foreign_keys = True

def
get_excluded_fields(self, instance):
if self.exclude_callback is not None:
return self.exclude_callback(instance)
else:
return []

def recurse_foreign(self, instance, field) -> dict:
retval = {}
if isinstance(field, models.OneToOneField):
# OneToOneField is a subclass of ForeignKey, thus must be placed
# before ForeignKey.
# If parent_link is true, then we need to pull in the fields
# as if they were part of the current model.
if not field.parent_link:
parent_obj = getattr(instance, field.name)
if parent_obj is not None:
value = self.model_to_dict(
parent_obj,
exclude=self.get_excluded_fields(parent_obj)
)
retval.update(value)
elif isinstance(field, models.ForeignKey):
# Resolve the model pointed to.
foreign = getattr(instance, field.name)
if foreign is not None:
value = self.model_to_dict(foreign)
retval[field.name] = value
else:
retval[field.name] = None
elif
isinstance(field, models.ManyToManyField):
# Create a list of model dicts.
modlist = []
related = getattr(instance, field.name)
for rel in related:
modlist.append(self.model_to_dict(rel))

retval[field.name] = modlist
else:
raise TypeError('recurse_foreign called on {}'.format(type(field)))

return retval

def link_foreign(self, instance, field) -> dict:
if isinstance(field, models.ManyToManyField):
return self.recurse_foreign(instance, field)
elif isinstance(field, models.OneToOneField):
if not field.parent_link:
return self.recurse_foreign(instance, field)
elif isinstance(field, models.ForeignKey):
foreign = getattr(instance, field.name)
# raise ValueError(repr(foreign))
if foreign is not None:
if getattr(foreign, 'absolute_url', False):
return {
field.name: {
'text': force_text(foreign),
'link': foreign.absolute_url,
}
}
else:
return {
field.name: {
'text': force_text(foreign),
'pk': foreign.pk
}
}

return {}

def model_to_dict(self, instance, exclude=None, **kwargs) -> dict:
"""Convert a model instance to a dictionary of field names and values.

If the model has a method of the same name, that method is called for
three reasons:
#. Centralization. This method can be used in other parts of django
or an application to provide a consistent dictionary of the
model.
#. The default implementation only maps fields. If the model has
important attributes that are implemented as properties this
mixin will not find them.
#. Hiding of sensitive fields. The model is better equipped to
evaluate if a field contains sensitive information.

:param instance: the model instance to convert
:type instance: models.Model
:param exclude: list of fields to exclude from being sent
:type exclude: list
"""
exclude = exclude or self.get_excluded_fields(instance)
if hasattr(instance, 'model_to_dict') and \
callable(getattr(instance, 'model_to_dict')):
return instance.model_to_dict(exclude, **kwargs)
retval = {}
for field in instance._meta.fields:
if field.name in exclude:
continue
if
isinstance(field, (models.ForeignKey, models.ManyToManyField)):
if self.recurse_foreign_keys:
retval.update(self.recurse_foreign(instance, field))
else:
merge = self.link_foreign(instance, field)
if merge:
retval.update(merge)
else:
retval[field.name] = getattr(instance, field.name)

return retval

def default(self, obj) -> object:
"""Prepares a value for json representation.

Complex types that are not containers should be handled by this method.
:param obj:
:type obj: object
"""
if isinstance(obj, models.query.QuerySet):
return list(obj)
elif hasattr(obj, 'model_to_dict') and \
callable(getattr(obj, 'model_to_dict')):
return obj.model_to_dict(exclude=self.get_excluded_fields(obj))
elif isinstance(obj, models.Model):
return self.model_to_dict(obj)
else:
return super().default(obj)

--

Melvyn Sopacua

Alison P

unread,
Jun 8, 2017, 8:23:42 AM6/8/17
to Django users
Hi!

Ludovic, Melvyn, thank you for your responses. I could not make Melvyn's example work for me, but it put me on the right track.

Solution: right under the "reset the one time password" comment, add the line "user.backend = 'path.to.OneTimePasswordBackend' ", and everything works as it should be!

Have a nice day!
A.
Reply all
Reply to author
Forward
0 new messages