Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Schnouki
Last active February 10, 2021 02:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Schnouki/32603c54bcd126fa86ea1fa15ac1b82a to your computer and use it in GitHub Desktop.
Save Schnouki/32603c54bcd126fa86ea1fa15ac1b82a to your computer and use it in GitHub Desktop.
WhiteNoiseMiddleware that restrics access to sourcemaps to authorized users
import fnmatch
from django.conf import settings
from django.http import HttpResponseForbidden
from whitenoise.middleware import WhiteNoiseMiddleware
class AuthenticatedWhiteNoiseMiddleware(WhiteNoiseMiddleware):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.auth_paths = getattr(settings, 'WHITENOISE_AUTHENTICATED_PATHS', None) or []
self.auth_cookie = getattr(settings, 'WHITENOISE_AUTH_COOKIE', None)
self.auth_cookie_domain = getattr(settings, 'WHITENOISE_AUTH_COOKIE_DOMAIN', None)
self.auth_cookie_secure = getattr(settings, 'WHITENOISE_AUTH_COOKIE_SECURE', None)
def __call__(self, request):
response = super().__call__(request)
response = self.process_response(request, response)
return response
def process_response(self, request, response):
if self.auth_cookie and hasattr(request, "user") and request.user.is_staff:
# User is authorized: add the auth cookie.
response.set_signed_cookie(self.auth_cookie, "1",
domain=self.auth_cookie_domain,
secure=self.auth_cookie_secure,
httponly=True)
return response
def serve(self, static_file, request):
if self.auth_cookie:
# Configured to enable authentication, let's do it!
path = request.path_info
auth_needed = any(fnmatch.fnmatch(path, pattern) for pattern in self.auth_paths)
if auth_needed and not request.get_signed_cookie(self.auth_cookie, default=False):
# Not authenticated even if needed: too bad…
return HttpResponseForbidden()
return super().serve(static_file, request)
WHITENOISE_AUTHENTICATED_PATHS = ["*.map"]
WHITENOISE_AUTH_COOKIE = "__static_auth"
WHITENOISE_AUTH_COOKIE_DOMAIN = ".example.com" # Required if statics for www.example.com are on static.example.com
WHITENOISE_AUTH_COOKIE_SECURE = not DEBUG
@omarsumadi
Copy link

omarsumadi commented Nov 11, 2020

Hi! Awesome piece of code you have there.

I'm relatively new to Django and was trying to implement this and had a few questions. I'm not using a subdomain (so not using WHITENOISE_AUTH_COOKIE_DOMAIN as you stated since my statics are served on www.example.com), but I was wondering what was meant by "__static_auth".

In addition, I just started my Django project from scratch, turned DEBUG off, and collected static to try it out, and it seems like the only time this middleware is trigger is when there is an HTTP/HTTPS request to the static files, because none of my static files except for chrome's request for a favicon.ico are going into this middleware. Is this the case because I'm still on a local server and the files are still being hosted locally, or am I doing something wrong. In addition, the serve() function was never called, but again, it might be because I'm still locally hosting my server despite DEBUG being off.

Lastly, where should this middleware be placed, after the auth middlewares?

Thanks!

@Schnouki
Copy link
Author

Hi @omarsumadi,

I was wondering what was meant by "__static_auth".

It's the name of the cookie used to authenticate users for serving sourcemaps. Basically, this cookie will only be set (as a signed cookie) if a user is logged in and has the is_staff flag (line 23). Then, if the user requests a sourcemap (path that matches WHITENOISE_AUTHENTICATED_PATHS, checked line 35), it is only allowed if that cookie is present (line 36).

In addition, I just started my Django project from scratch, turned DEBUG off, and collected static to try it out, and it seems like the only time this middleware is trigger is when there is an HTTP/HTTPS request to the static files, because none of my static files except for chrome's request for a favicon.ico are going into this middleware. Is this the case because I'm still on a local server and the files are still being hosted locally, or am I doing something wrong. In addition, the serve() function was never called, but again, it might be because I'm still locally hosting my server despite DEBUG being off.

Yep. This is intended as a replacement of the Whitenoise middleware, so it will only be triggered for static files.

Lastly, where should this middleware be placed, after the auth middlewares?

It should be used instead of the Whitenoise middleware as explained in the Whitenoise documentation.

Sorry for not being more specific, but this code is several years old and I'm not using it anymore 😅

@omarsumadi
Copy link

Thank you so much - you solved all my problems! I'm not sure what I can do for you in return - I think stars are important on this platform, I'll go give you some!

@Schnouki
Copy link
Author

Glad it helped! :)

@omarsumadi
Copy link

omarsumadi commented Nov 26, 2020

Hey Schnouki, sorry for bringing this up again. As a consolation - I'd be happy to donate to you!

I know that in your process_response function, you ask if request.user.is_staff, and that you instructed me to place this custom middleware as a replacement for the whitenoise middleware.

I tried out your code more rigorously, and I couldn't get Django to check request.user for authentication unless I put the import (as a custom middleware) beneath the django Authentication middlewares. As stated in the issue that you replied to, "to check whether a user is authenticated, (for Django at least) the WhiteNoise middleware would need to be placed after the Django session/authentication middleware, which makes database requests etc. This would negate the performance benefits of WhiteNoise."

I would be breaking the spirit of your code, and was wondering if this was intended by you - you originally coded this to avoid this issue of degrading performance, but I can't seem to avoid this performance degradation as you have because I can't get Django to check if the user is authenticated without the following changes:

Here's the structure of my Middlewares:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    #"whitenoise.middleware.WhiteNoiseMiddleware", (where Whitenoise should go according to the documentation, and as you told me)
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "users.middleware.ProtectedStaticFileMiddleware", # my custom middleware from you, where I have to put it for this to work.
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.common.BrokenLinkEmailsMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

In addition, I was wondering if you knew how to "replace the caching headers on authenticated responses", I've been trying but only to failure.

Thanks,
Omar

@Schnouki
Copy link
Author

Schnouki commented Dec 2, 2020

I just checked the project where I first added this middleware. It is definitely before AuthenticationMiddleware in the list, as it is actually a hack to avoid having to authenticate every request with Django. Instead, logging in (for another request) will set the auth cookie, which can then be used by this middleware without depending on Django's authentication.

Here's the thing: middlewares are processed from top to bottom when processing incoming requests, then from bottom to top when processing responses (see the schemas on this page). So here, the auth cookie is added when processing the response of a logged-in request. So if you login and browse any page on your site, the process_response() method of that middleware should be called, and it should set the auth cookie.

Then, new incoming requests (for static files) will have that cookie in the request, which will allow you to serve protected static files.

No idea about caching headers. My whole static file stuff (webpack + whitenoise) was setup to add hashes to filenames, so caching was not really an issue...

@omarsumadi
Copy link

Thank you so much for helping me through this from years ago to now. All this makes sense and this code should work in my project - I realize now the mistakes are something I need to correct from my own end.

Unfortunately, I implemented exactly what you are describing, but still I yet to be able to check if the request is authenticated in process_response without it raising an error or exception. Maybe this is because I am using ASGI in django?

I'm going to keep cracking at this, but I think you have given me all the information I need.

Thanks a million!

@omarsumadi
Copy link

omarsumadi commented Dec 2, 2020

django          |   File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
django          |     response = get_response(request)
django          |   File "/app/edsproject/users/middleware.py", line 72, in __call__
django          |     response = self.process_response(request, response)
django          |   File "/app/edsproject/users/middleware.py", line 77, in process_response
django          |     print(request.user.is_authenticated)
django          | AttributeError: 'ASGIRequest' object has no attribute 'user'

Oh - I think perhaps I misinterpreted this whole project. Here's what I learned:

Initial Expectation:

  • On every static file request, we check if the user is authenticated. Because we are using cookies, we are somehow increasing performance, but I really didn't understand what is actually going on.

Actuality:

  • We go to the Login URL, we log in. (In the very beginning)
  • Response is eventually routed to process_response, and an authentication cookie is set as per your code. (this is now the main point of confusion, I need to make sure that my authentication is going to trigger the cookie being set - so is it any response by me from views is going to trigger the cookie setting? For instance, I authenticate my user in my views, is it going to route to process_response after the view is completed to add the cookie?).
  • Any subsequent times we are logged in and access a URL that leads to a view, the authentication cookies is added/refreshed via routing eventually to process_response when any response is being sent back from views.

On Static files

  • We placed Whitenoise middleware / now our custom middleware before authentication middleware's, so any requests that go through (as you said requests are propagated through middleware's linearly, not backwards like process_response) will NOT be present (getattr request user).
  • Because we are intending for Whitenoise to NOT hit authentication, and the fact that whitenoise will be first triggered before authentication middleware's, all requests for the user attribute is going to throw an exception/not be there.
  • However, because we set a cookie for authentication, we can just check that cookie instead without needing the authentication framework to be in place.
  • Then we check file paths with that cookie's status as signed or not acting as our weapon of authentication.

This explains why requests to URLs -> URLs had process_response working as normal, but for staticfiles, process_response was triggering the above error. But the above error is supposed to be there because we aren't checking authentication via Django but via cookies to bypass the authentication in total. Again, we placed the middleware before authentication thus any requests will NOT have authentication checking capabilities.

Sorry - I'm so new to all this i'm just having a hard time with the entire process of execution to get this working. Again, sorry for bothering you a lot about this - I'm happy to donate as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment