How to Serve Protected Content With Django (Without Bogging Down Your Application Server)

Using Nginx's X-Accel-Redirect you can apply permissions to files served directly by Nginx or combine Django and WordPress in the same URL paths.

Like Apache’s mod_xsendfile, Nginx’s
X-Accel module provides for internal
redirects. An x-accel-redirct is internal because instead of
redirecting the client’s request to another URL, it redirects the
location of Nginx’s request to another resource.

This is an extremely useful feature because it lets you combine your
application’s logic with Nginx’s excellent file handling without
sacrificing one for the other.

Note: while this post is directly concerned with using x-accel-redirects
with Django, nothing about this feature or these patterns should be
construted as Django or Python specific. The lessons here are applicable
to any framework on language.

Serving protected files

This is probably the most useful scenario. Let’s say you have files you
want served to users but they need to be authenticated, and moreover you
want to check fi those users have the permission to access these files.

We’ll take this model definition as the base of our example, with the
understanding the functions referenced below, slugify and get_extension,
are imported from Django’s default filter library and previously
defined, respectively.

Now the goal is to write a view that will only serve this document to
authenticated users. Because we’re using Django’s authentication system,
contrib.auth,
we’ll just use the login_required decorator. Clearly in your own app
you’ll likely have app specific permission logic. Simply redirecting to
the file URL isn’t acceptable because that means the file URL, obscure
as it may be, does not require authentication. So serving the file must
be controlled by the view.

Well this view certainly does that, but boy is it horrible. The
application is opening and reading Bob’s 24MB PowerPoint deck just to
make sure the user is logged in. This is a terrible use of resources.

It’d be much better to serve this document with Nginx because Nginx is
really fast at serving files. So we’ll use it. Except we can’t point
Nginx directly at the file to serve it as if it were a file in the web
root directory. So let’s dive right in and use the redirect.

First, here’s an updated location in the Nginx site configuration:

location/protected/{internal;alias/usr/local/documents;}

Now with this location defined, let’s take another crack at the view
function:

The first thing you should notice about the HttpResponse in the second
view is that it doesn’t have any content. All we’ve done is set two
headers, one to indicate that the response is an attachment with a name,
and the second which is the x-accel-redirect location. When Nginx
detects the x-accel-redirect header it will redirect the response before
forwarding it to the client.

This lets your application do its thing, whether that’s checking
permissions, updating a download count, or emailing you to let you know
which students actually downloaded the homework assignment, and then
hand off the job of actually serving the file to Nginx.

This is useful in scenarios where you want to guard files and others
where you want to disguise the file name/path.

The Django application does it’s auth and permission checks like in the
first example, but now instead of responding with the file, it sends an
HTTP response with the x-accel-redreict ehader and the path to the file.
This is an internal redirect. The user’s URL never changes, and Nginx
serves the file from the internally protected directory.

Serving protected upstream services

This feature to serve protected files is extremely useful, but
it’s not the end of the road. In the intro I mentioned that we can use
this to have Nginx redirect responses to different resources, and that’s
exactly what we’re going to do here.

Typically when you have multiple types of sites, like a marketing site,
a blog, and an application, you provide each with its own domain
regardless of what servers they’re running on. This then has some obvious
downsides for URLs. You need app.mydomain.com and blog.mydomain.com, and
so on.

I know how to solve that, you say, you just assign specific logical
paths in the site configuration.

Not only may that be a royal pain-in-the-ass, it may not be possible without
assigning one application’s root domain to some non-root path. If you’re
using your blogging platform for static pages, too, does every URL now
start with a shared prefix like /blog/ or /pages/?

So what I mean is deploying using the same root path.

Maybe you want to one page for non-logged in users, a public facing site
managed by the marketing team, and then access to an application
dashboard and data accessible to users and a different team.

Start with an app with a dashboard, and add a WordPress site into the
mix. The WordPress site is managed by the marketing team and is used to
provide information about the company as well as a product blog. The
transition from system to system should look seamless to users.

If the user is authenticated then the dashboard page is rendered
normally, otherwise the response is an internal redirect to the
WordPress site.

Here’s a simple example of a full Nginx site configuration to
accommodate this strategy:

upstreamexample{serverlocalhost:5000;}upstreamphp{serverlocalhost:9000;}server{listen80;client_max_body_size4G;server_namewww.example.comroot/var/www/wordpress;indexindex.php;access_log/var/log/nginx/example.access.log;error_log/var/log/nginx/example.error.log;keepalive_timeout5;# We know this URL, so ensure that it's forwardedlocation/wp-admin{try_files$uri$uri//wp-admin/index.php;}location/wordpress/{internal;try_files$uri$uri//index.php;}location/{try_files$uri@example;}location~\.php${#NOTE: You should have "cgi.fix_pathinfo = 0;" in php.iniincludefastcgi.conf;fastcgi_intercept_errorson;fastcgi_passphp;}location@php{includefastcgi.conf;fastcgi_intercept_errorson;fastcgi_passphp;}location@example{proxy_set_headerX-Forwarded-For$proxy_add_x_forwarded_for;proxy_set_headerHost$http_host;proxy_redirectoff;proxy_passhttp://example;}}

All things being equal I’d prefer separate domains myself. It’s just
that much easier to manage these services when they’re wholly decoupled.
However sometimes other concerns must rule the day.

Dynamic X-Accel-Redirect routing

Reconsidering the example with our combined Django app and WordPress
site, we did specify two paths for the WordPress admin: the index page
and the admin interface, /wp-admin. So what about access to the
additional pages, like an about page?

While I’d recommend trying to specify known primary paths in your Nginx
configuration, it’s simple to redirect to additional WordPress pages
using a custom 404 handler. This will redirect missing requests to the
WordPress site, like /about/, and rely on WordPress to respond to truly
missing pages.