6

I have enabled user authentication with DRF using TokenAuthentication

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
         'rest_framework.authentication.TokenAuthentication',
         'rest_framework.authentication.SessionAuthentication'
    ),
    'DEFAULT_MODEL_SERIALIZER_CLASS':
        'rest_framework.serializers.ModelSerializer',
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.AllowAny',
    ),
    #'EXCEPTION_HANDLER': 'apps.core.exceptions.custom_exception_handler'

}

I have the following model:

class Device(CreationModificationMixin):
    """
    Contains devices (WW controllers).  A device may be associated with the Owner
    """
    _STATUSES = (
        ('A', 'Active'), # when everything is okay
        ('I', 'Inactive'), # when we got nothing from SPA controllers for X minutes
        ('F', 'Failure'), # when controller says it has issues
    )

    _TYPES = (
        ('S', 'Spa'),
        ('P', 'Pool'),
    )

    udid    = models.CharField(max_length=255, verbose_name="Unique ID / MAC Address", help_text="MAC Address of WiFi controller", unique=True, null=False, blank=False, db_index=True)
    type    = models.CharField(max_length=1, choices=_TYPES, null=False, blank=False)
    title   = models.CharField(max_length=255, null=False, blank=False, db_index=True)
    status  = models.CharField(max_length=1, default='A', choices=_STATUSES)
    pinged  = models.DateTimeField(null=True)
    owner   = models.ForeignKey(Owner, verbose_name="Owner", null=True, blank=True, db_index=True)

    def __str__(self):
        return self.udid

This represents hardware device that will be sending discrete requests to API endpoints, therefore I need to authenticate each request and ideally with token based identification, like

POST /api/devices/login 
{
   udid: '...mac address...',
   hash: '...sha256...hash string',
   time: '2015-01-01 12:24:30'
}

hash will be calculated on device side as sha256(salt + udid + current_time) the same hash will be calculated on DRF side inside /login to compare and generate token that will be saved in REDIS and returned back with response.

All future requests will be passing this token as a header, which will be checked in custom Permission class.

my questions:

  1. I'd like to set a custom property on request class, like request.device, request.device.is_authenticated()

Where should I put this functionality?

  1. Do you see something wrong in my approach? Maybe a recommendation for improvements?
1
  • This is a duplicate of this question, but I'm currently out of flags. Commented Jun 24, 2015 at 23:48

2 Answers 2

10

As @daniel-van-flymen pointed out, it's probably not a good idea to return a device instead of a user. So what I did was create a DeviceUser class that extends django.contrib.auth.models.AnonymousUserand return that in my custom authentication (devices are essentially anonymous users, after all).

from myapp.models import Device
from rest_framework import authentication 
from django.contrib.auth.models import AnonymousUser 
from rest_framework.exceptions import AuthenticationFailed 

class DeviceUser(AnonymousUser):

    def __init__(self, device):
        self.device = device 

    @property 
    def is_authenticated(self):
        return True 


class DeviceAuthentication(authentication.BaseAuthentication):

    def authenticate(self, request):
        udid = request.META.get("HTTP_X_UDID", None)
        if not udid:
            return None 

        try:
            device = Device.objects.get(udid=udid)
        except Device.DoesNotExist:
            raise AuthenticationFailed("Invalid UDID")

        if not device.active:
            raise AuthenticationFailed("Device is inactive or deleted")

        request.device = device 
        return (DeviceUser(device), None)

This code lives in myapp.authentication, you can then add the following to your settings:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "myapp.authentication.DeviceAuthentication", 
    )
}

A couple of notes from your original spec: I've modified the request in the authenticator to include the device, so you can do request.device.is_authenticated; however, the user will be a DeviceUser so you could also do request.user.device.is_authenticated (so long as you do the appropriate checks for the device attribute).

Your original spec also asked to implement TokenAuthentication, and it is possible to subclass this authentication class to use it more directly; for simplicity, I'm just having the device include the X-UDID header in their request.

Also note that as with the token authentication mechanism, you must use this method with HTTPS, otherwise the UDID will be sent in plain text, allowing someone to impersonate a device.

Sign up to request clarification or add additional context in comments.

Comments

1

You can subclass DRF's BaseAuthentication class and override the .authenticate(self, request) method. On successful auth this function should return (device, None). This will set device object in request.user property. You can implement is_authenticated() in your Device model class.

class APICustomAuthentication(BaseAuthentication):
    ---
    def  authenticate(self, request):
        ----
        return (device, None)    # on successful authentication

Add APICustomAuthentication to 'DEFAULT_AUTHENTICATION_CLASSES' in settings.

More details are available here

1 Comment

This isn't a good idea because if you return a device then request.user is going to be a device in your context.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.