Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Authentication for non-user connection with Django Rest Framework

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?
like image 470
DmitrySemenov Avatar asked Jun 24 '15 00:06

DmitrySemenov


2 Answers

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.

like image 115
bbengfort Avatar answered Sep 22 '22 13:09

bbengfort


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

like image 33
Rajesh Kaushik Avatar answered Sep 21 '22 13:09

Rajesh Kaushik