/ python

Basic Auth en Django

Como hacer tu propio middleware en Django para proteger la aplicación con Basic Authentication.

¿Porqué?

Dependiendo de tus necesidades, puede que tengas una aplicación que será consumida dentro de una VPN para una intranet, y es posible que no cuenten con un OAuth server, por lo que recurrimos a tener el mínimo necesario de seguridad, que en este caso sería un Basic Authentication

Es posible que consideres que esto debería estar en un web server y de ahí pasarlo a la aplicación, pero si somos desconfiados y queremos asegurarnos que siempre tenemos el mínimo requerido de seguridad, implementamos el Basic Auth en nuestra app

Herramientas (middleware)

En Django existe el concepto de middleware, el cual, posiblemente conozcas como un filtro, en su forma más simple, este middleware es un fragmento de código que quieres ejecutar antes y/o después de cada request, por ejemplo, para nuestro caso, queremos ejecutar este código antes de cualquier request para validar si tenemos la autenticación, de ser así, continuaremos normalmente, de lo contrario, retornamos un error

Comencemos

Dentro de nuestra aplicación, creemos un nuevo módulo de middlewares para almacenar ahí cualquier elemento de esta naturaleza que querramos desarrollar

myapp
    >> middlewares
        >> basicauth.py

Ahora vamos al archivo basicauth.py y comencemos

# no es necesario extender de alguna clase en particular
class BasicAuthMiddleware:
    '''
    Middleware to easily validate the Authorization headers
    to make sure the client is using the basic auth.
    '''
    
    def __init__(self, get_response):
        # Django pasa esta funcion al momento de instanciar nuestra
        # implementacion para que podamos utilizarla para obtener la
        # respuesta con el "flujo normal"
        self.get_response = get_response

Vamos a sobreescribir el método __call__, esta función es llamada por Django antes de ir al view handler que hayamos definido para la URL

def __call__(self, req):
    # validar el request previo al view handler!
    err = self.get_error_if_unauthorized(req)
    if err is not None:
        return err
    
    # obtener la respuesta usual con el handler designado
    # usando la funcion que recibimos desde el __init__ function
    res = self.get_response(req)
    return res

Okay, ya tenemos la estructura básica, ahora implementemos el método get_error_if_unauthorized

def get_error_if_unauthorized(self, req):
    '''
    Validates if the Authorization header contains the proper
    credentials for the app.
    '''
    # no es la mejor forma de hacerlo, pero vamos a ejecutar el codigo
    # de forma secuencial y cualquier error sera considerado como un
    # acceso no autorizado
    try:
        # el formato del header es: Basic TOKEN, por lo que obtenemos
        # únicamente el token aquí
        header = req.META['HTTP_AUTHORIZATION'].split(' ')[1]
        # para este caso, el token esta codificado con base64,
        # al decodificarlo, tenemos el formato: USERNAME:PASSWORD
        plain = base64.b64decode(header).decode('utf-8').split(':')
        username_plain = plain[0]
        password_plain = plain[1]
        # las credenciales definidas en nuestro settings.py
        username = getattr(settings, 'BASIC_AUTH_UNAME', 'admin')
        pwd = getattr(settings, 'BASIC_AUTH_PWD', 'nimda')
        if username != username_plain or pwd != password_plain:
            raise Exception('Credenciales incorrectas!')
    except:
        # esto puedes customizarlo a retornar un 403 o 401, acorde a tu lógica
        # por ejemplo, validando nivel de permisos por usuario
        # para mantener el ejemplo sencillo, vamos a dejarlo así :)
        return HttpResponse(status=401)
        

Juntando las partes

import base64
from django.conf import settings
from django.http import HttpResponse

class BasicAuthMiddleware:
    '''
    Middleware to easily validate the Authorization headers
    to make sure the client is using the basic auth.
    '''
    
    def __init__(self, get_response):
        self.get_response = get_response
        
    def __call__(self, req):
        # validar el request previo al view handler!
        err = self.get_error_if_unauthorized(req)
        if err is not None:
            return err

        # obtener la respuesta usual con el handler designado
        # usando la funcion que recibimos desde el __init__ function
        res = self.get_response(req)
        return res
        
    def get_error_if_unauthorized(self, req):
        '''
        Validates if the Authorization header contains the proper
        credentials for the app.
        '''
        try:
            header = req.META['HTTP_AUTHORIZATION'].split(' ')[1]
            plain = base64.b64decode(header).decode('utf-8').split(':')
            username_plain = plain[0]
            password_plain = plain[1]
            username = getattr(settings, 'BASIC_AUTH_UNAME', 'admin')
            pwd = getattr(settings, 'BASIC_AUTH_PWD', 'nimda')
            if username != username_plain or pwd != password_plain:
                raise Exception('Credenciales incorrectas!')
        except:
            return HttpResponse(status=401)

Instalando el middleware

Para instalarlo, sólo es necesario ir a settings.py y ajustar la variable MIDDLEWARE

MIDDLEWARE = [
  # las cosas que vienen por default
  'middlewares.basicauth.BasicAuthMiddleware'
]

# finalmente agregamos las variables que necesitamos para
# configurar nuestro basic authentication system!! :)
BASIC_AUTH_UNAME=yomama
BASIC_AUTH_PWD=sofat

Para más especificaciones del middleware, pueden leer la documentación oficial aquí