Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Security issues with a middleware screener page

Tags:

python

django

While my startup is in dark mode I want all access except access to / to go to a screener page where users have enter a password given to them by a representative. I've come up with the following simple middleware to perform the task. Just to be clear, this is intended to ensure that users agree to keep the site in confidence before they are allowed to browse around rather than for use as a security system or .htaccess clone. However I would like to prevent them seeing any public pages (i.e those which are not decorated with @login_required) without knowing the screener password. The password_check function uses Django Auth to generate the hash of the input password to check against a db value.

Any thoughts/circumvention techniques that you guys can see? One idea I had was to change the login function to push the LicenceKey into the newly logged in users session, rather than giving logged in users an exemption. However since they can only create a new session by logging in, and logging in requires agreeing to the screener, it seems redundant.

Feedback appreciated.

The middleware looks like this:

from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
import re

class LicenceScreener(object):
    SCREENER_PATH = reverse("licence")
    INDEX_PATH = reverse("index")
    LICENCE_KEY = "commercial_licence"
    def process_request(self, request):
        """ Redirect any access not to the index page to a commercial access screener page
            When the screener form is submitted, request.session[LICENCE_KEY] is set.
        """
        if not LicenceScreener.LICENCE_KEY in request.session \
            and not request.user.is_authenticated() \
            and LicenceScreener.SCREENER_PATH != request.path\
            and LicenceScreener.INDEX_PATH != request.path:
                return HttpResponseRedirect(self.SCREENER_PATH)

And the view looks like this:

def licence(request):
    c = RequestContext(request, {}  )
    if request.method == 'POST':
        form = LicenceAgreementForm(request.POST)
        if form.is_valid():
            if password_check(form.cleaned_data["password"]):
                request.session[LicenceScreener.LICENCE_KEY] = True
                return HttpResponseRedirect(reverse("real-index"))
            else:
                form._errors["password"] = form.error_class([_("Sorry that password is incorrect")])    
    else:
        form = LicenceAgreementForm()  
    c["form"] = form        
    return render_to_response('licence.html',c)

EDIT 1. Removed the Regexes as suggested by Tobu

like image 948
Erg0sum Avatar asked Jun 20 '11 08:06

Erg0sum


1 Answers

Here is my solution. It is using not COOKIES but custom Auth Backend.

Step 1

It's good to have a Django application for this:

key_auth/
    templates/
        key_auth_form.html # very simple form template
    __init__.py
    models.py
    urls.py
    views.py
    forms.py
    middleware.py
    backend.py

settings.py:

INSTALLED_APPS = (
    # ...
    'key_auth',
)

Step 2

We need a Model to store your tokens. models.py:

from django.db import models
from django.contrib.auth.models import User

class SecurityKey(models.Model):
    key = models.CharField(max_length=32, unique=True)
    user = models.OneToOneField(User)

Note: In my simple solution you will need to create and synchronize new Users and their SecurityKeys manually. But you can improve this in future.

Step 3

We need a custom Middleware that will require authentication from all users at all pages (except few special pages). Here is the middleware.py:

from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse

class KeyAuthMiddleware(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        login_url = reverse('key_auth_login') # your custom named view

        # Exceptional pages
        login_page = request.path.find(login_url) == 0
        logout_page = request.path.find(reverse('logout')) == 0
        admin_page = request.path.find(reverse('admin:index')) == 0 # I've excluded Admin Site urls

        if not login_page and not logout_page and not admin_page:
            view_func = login_required(view_func, login_url=login_url)

        return view_func(request, *view_args, **view_kwargs)

This middleware will redirect unauthorized users to the 'key_auth_login' page with auth form in it.

Step 4

Here are urls.py which maps 'key_auth_login' view:

from django.conf.urls.defaults import patterns, url

urlpatterns = patterns('key_auth.views',
    url(r'^$', 'login_view', name='key_auth_login'),
)

And the global project's urls.py:

from django.contrib import admin
from django.conf.urls.defaults import patterns, include, url

admin.autodiscover()

urlpatterns = patterns('',
    url(r'^key_auth/$', include('key_auth.urls')),
    url(r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': '/'}, name='logout'),
    url(r'^admin/', include(admin.site.urls)),
    url(r'^$', 'views.home_page'),
)

As you can see - Admin Site is on (so you can also log in as admin).

Step 5

Here is our view (views.py):

from django.contrib.auth import login
from django.views.generic.edit import FormView
from key_auth.forms import KeyAuthenticationForm

class KeyAuthLoginView(FormView):
    form_class = KeyAuthenticationForm
    template_name = 'key_auth_form.html'

    def form_valid(self, form):
        login(self.request, form.user)
        return super(KeyAuthLoginView, self).form_valid(form)

    def get_success_url(self):
        return self.request.REQUEST.get('next', '/')

login_view = KeyAuthLoginView.as_view()

I will not show 'key_auth_form.html' because it's really simple form template, nothing special. But I'll show form class

Step 6

Form class (forms.py):

from django import forms
from django.contrib.auth import authenticate

class KeyAuthenticationForm(forms.Form):
    key = forms.CharField('Key', help_text='Enter your invite/authorization security key')
    user = None

    def clean_key(self):
        key = self.cleaned_data['key']
        self.user = authenticate(key=key)
        if not self.user:
            raise forms.ValidationError('Please, enter valid security key!')
        return key

As we see, authenticate() used here. This method will try to authenticate user with existent backends.

Step 7

Custom authentication backend. backend.py:

from django.contrib.auth.models import User
from key_auth.models import SecurityKey

class KeyAuthBackend(object):
    def authenticate(self, key=None):
        try:
            return SecurityKey.objects.get(key=key).user
        except SecurityKey.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

It's very simple! Just check the key (token). Here is it's installation (settings.py):

AUTHENTICATION_BACKENDS = (
    'key_auth.backends.KeyAuthBackend',
    'django.contrib.auth.backends.ModelBackend',
)

The second one is left to keep Admin Site working.

Resume

That's it!

  1. Any request is going through the KeyAuthMiddleware
  2. KeyAuthMiddleware skips login, logout and Admin urls ...
  3. ... and redirects all unauthorized users to the token-auth login page (with token-login form)
  4. This form validates 'key' through the django.contrib.auth.authenticate() method ...
  5. ... which tries to authenticate user via custom KeyAuthBackend authentication backend
  6. If auth is successful and form is valid then custom KeyAuthLoginView makes django.contrib.auth.login(user) and redirects to the requested page

You're also able to use Admin Site and login/logout views free of key_auth ckeck.

like image 115
Rost Avatar answered Sep 17 '22 19:09

Rost