abidibo.net

Serve private media with angular, DRF, CORS and token authentication

angularjs django drf rest

A long time has passed since my last post, I was fighting hard with a project learning django REST framework and angularjs, and now time has come to share some interesting knowledges.

This entry talks about serving private media from a django REST webservice, developed with django REST framework, to angularjs, in a scenario which uses a token authentication schema and CORS requests.

The problems to face

Let's see the problems we have to deal with.

  • Media with django are normally served directly by the web server, so they are out of the authentication/permission logic of the application
  • The token authentication schema needs every request to contain an header 'Authentication' part providing the token key (also when requesting files)
  • Allowing CORS roughly means to add headers in the response allowing asyncronous communication between different domains.

Let's solve them all

The first thing to do is to find a way to serve media in django relying upon the authentication/permission logic. I've choosen to use a django app named private_media written by Racing Tadpole.

It does exactly what I need, except from the fact that it implements the apache way of doing such thing, while I need the nginx way, but we'll see this will not be a great problem (actually in the github project nginx is covered but installing it with pip I got an older version, so I've written my own nginx server class).

To set up the private media application we need to set some settings in our settings.py:

PRIVATE_MEDIA_URL = '/private/'
if DEBUG:
    # dev
    import os
    PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
    PRIVATE_MEDIA_ROOT = os.path.join(PROJECT_PATH, 'private')
    PRIVATE_MEDIA_SERVER = 'private_media.servers.DefaultServer'
else:
    # prod
    PRIVATE_MEDIA_ROOT = '/home/user/my/path/to/private/media'
    #PRIVATE_MEDIA_SERVER = 'private_media.servers.ApacheXSendfileServer'
    PRIVATE_MEDIA_SERVER = 'private_media.servers.NginxServer'         

PRIVATE_MEDIA_PERMISSIONS = 'myapp.permissions.MyPermissionClass'

As you can see, I've commented out the "Apache" part and introduced an "Nginx" part. We'll need to write the NginxServer class and add it to the servers.py file of the private_media app.

There's a difference between DEBUG modes, since when DEBUG is TRUE files are served directly from the django development server.

With the PRIVATE_MEDIA_PERMISSIONS setting we set the class containing the authentication logic. It must implement a method called:

has_read_permission(self, request, path)

Which returns True if the user can access the file, otherwise False or any exception. In my case such class was a bit more complicated, because I can't rely over request.user, while I have to get the authentication token directly from the request headers:

class MediaPermissions(object):
    """
    Access permission to all private media files
    Token key must be read from request object since this view is not routed by the rest framework
    """
    def has_read_permission(self, request, path):
        try:
            token_key = request.META['HTTP_AUTHORIZATION'].rsplit(' ', 1)[1]
            token = Token.objects.get(key=token_key)
            request.user = token.user
            res_folder = os.path.split(os.path.split(os.path.split(path)[0])[0])[0]
            if(res_folder == 'cases'):
                #...
                return True # or False
            elif(res_folder == 'doctors'):
                return request.user.is_authenticated()
            raise PermissionDenied('Forbidden')
        except:
            raise PermissionDenied('Forbidden')

Then we can add the private_media app in the INSTALLED_APPS and add such line in urls.py

...
url(r'^', include('private_media.urls')),

In your models.py, to upload a specific file or image to a private area, use:

from django.db import models
from private_media.storages import PrivateMediaStorage

class Car(models.Model):
    photo = models.ImageField(storage=PrivateMediaStorage())

Now we have to set up nginx in order to serve the protected files. We'll use the internal directive, which causes a 404 when the file is accessed from the outside.

server {
        listen 80;
        server_name example.com;
        access_log /var/log/nginx/example.com/access.log;

        location /static { 
                root /home/example/sites/example/shared;
        }
        location /media {  
                root /home/example/sites/example;
        }
        location /protected {
                add_header Access-Control-Allow-Origin *;
                alias /home/example/sites/example/private;
                internal; 
        }
        location / {
                uwsgi_pass unix:/tmp/uwsgi_example.sock;
                include /etc/nginx/uwsgi_params;  
        }
    
}

The bold rules:

  • prevent the protected files to be accessed directly,
  • add the Access-Control-Allow-Origin header in order to enable CORS
  • set an alias pointing to the real filesystem path where files reside.

You can see the url matched here is '/protected', while the PRIVATE_MEDIA_URL is '/private/'. The reason is that we have to use different urls, one used to request the file which will be caught by the django app which then runs the authentication logic, and the one used internally by the django app to serve it (and which is instead hidden from the outside thanks to the internal directive).

So the NginxServer class must take into account such urls, and here comes my implementation:

class NginxServer(object):

    def serve(self, request, path):

        response = HttpResponse()
        del response['Content-Type']
        nginx_path = os.path.join('/protected', path)
        response['X-Accel-Redirect'] = nginx_path
        response['Content-Type'] = ''
        return response

Just add this class to server.py file of the private_media app.

As you can see, we use the X-Accel-Redirect header in order to access internally the protected file, and we alter the path taking into account what I've just explained above.

So what will happen?

  1. We request a file http://example.com/private/path/to/file
  2. the private_media.urls module catches the request (which matches the PRIVATE_MEDIA_URL setting)
  3. The permission class is run to see if the user can access the file
  4. If the user can access the file, an header is added to the response so that the file is accessed internally and served to the user

What the hell has to do angular with all such things?

Well, I said that I've developed a token authentication schema, in which angular adds an Authentication header to every API request, in order to send the token key. But when you add a link in your html template, pointing directly to a private file, the browser will manage the request and the token will not be included, so the authentication logic will always fail.

The solution here is to request the file through ajax, get a blob and make the browser open it (or display it if it is an image). For this reason I've written two angular directives, one to display protected images and the other to display/download other kinds of file.

(function () {
    'use strict';

    angular
        .module('example.utils.directives', [])
        .directive('imgajax', imgajax)
        .directive('aajax', aajax);

    aajax.$inject = ['files'];
    imgajax.$inject = ['files'];

    /**
    * @namespace   aajax
    * @description Directive used to link external files which have to be retrieved using ajax
    *              Private media must be retrieved using ajax calls because the auth token must
    *              be included in the request header. Then the response must be used as a Blob
    *              in order to allow download or browser visualization
    * @returns {Directive}
    */
    function aajax(files) {
        return {
            restrict: 'E',
            replace: false,
            transclude: true,
            scope: {
                href: "@href"
            },
            template: '<a href ng-transclude></a>',
            link: function(scope, elem, attrs) {
                elem.bind('click', function() {
                    files.getMedia(attrs.href).then(function(response) {
                        var blob = response.data;
                        var objectUrl = URL.createObjectURL(blob);
                        window.open(objectUrl);
                    });
                });
            }
        };
    }

    /**
    * @namespace   imgajax
    * @description Directive used to show external images which have to be retrieved using ajax
    *              Private media must be retrieved using ajax calls because the auth token must
    *              be included in the request header. Then the response must be used as a Blob
    *              in order to allow download or browser visualization
    * @returns {Directive}
    */
    function imgajax(files) {
        return {
            restrict: 'E',
            replace: false,
            transclude: true,
            scope: true,
            template: '<img class="{{ class }}" ng-src="{{ data }}" />',
            link: function(scope, elem, attrs) {
                // init value
                scope.data = 'assets/img/photo_placeholder.png';
                scope.class = attrs.class;
                attrs.$observe('iaSrc', function(value) {
                    if(value !== '') {
                        files.getMedia(value).then(function(response) {
                            var blob = response.data;
                            scope.data = URL.createObjectURL(blob);
                        });
                    }
                });
            }
        };
    }

})();

Both use a simple service to request the protected files:

/**
 * Performs an ajax get request to get a private media
 * @param {String} url
 * @return {Promise}
 */
function getMedia(url) {
    return $http({method: 'GET', url: url, responseType: "blob"});
}

Here it is important to set the responseType as blob, in order to use the

URL.createObjectURL(blob)

javascript function which will provide you with an url that opened in a new window displays or downloads the file.

When displaying images, it is enough to set the ng-src attribute as the received blob data.

You can simply use them this way:

<!-- link to a protected file -->
<aajax href="{{ a.file }}">bla bla</aajax>
<!-- protected img -->
<imgajax ia-src="{{ a.photo }}" class="img-responsive img-circle"></imgajax>

Hope you enjoy this entry, see you in september!

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured