Django: Basic Auth for one view (avoid middleware)
Asked Answered
W

5

24

I need to provide http-basic-auth to one view.

I want to avoid modifying the middleware settings.

Background: This is a view which gets filled in by a remote application.

Weems answered 26/9, 2017 at 12:31 Comment(0)
W
7

This library could be used: https://github.com/hirokiky/django-basicauth

Basic auth utilities for Django.

The docs show how to use it:

Applying decorator to CBVs

To apply @basic_auth_requried decorator to Class Based Views, use django.utils.decorators.method_decorator.

Source: https://github.com/hirokiky/django-basicauth#applying-decorator-to-cbvs

Weems answered 13/10, 2017 at 13:49 Comment(1)
how do I apply it to admin site?Sirdar
K
14

When you do a basic auth request, you're really adding credentials into the Authorization header. Before transit, these credentials are base64-encoded, so you need to decode them on receipt.

The following code snippet presumes that there's only one valid username and password:

import base64

def my_view(request):
    auth_header = request.META.get('HTTP_AUTHORIZATION', '')
    token_type, _, credentials = auth_header.partition(' ')

    expected = base64.b64encode(b'username:password').decode()

    if token_type != 'Basic' or credentials != expected:
        return HttpResponse(status=401)

    # Your authenticated code here:
    ...

If you wish to compare to the username and password of a User model, try the following instead:

def my_view(request):
    auth_header = request.META.get('HTTP_AUTHORIZATION', '')
    token_type, _, credentials = auth_header.partition(' ')

    username, password = base64.b64decode(credentials).split(':')
    try:
        user = User.objects.get(username=username)
    except User.DoesNotExist:
        return HttpResponse(status=401)

    password_valid = user.check_password(password)

    if token_type != 'Basic' or not password_valid:
        return HttpResponse(status=401)

    # Your authenticated code here:
    ...

Please note that this latter version is not extremely secure. At first glance, I can see that it is vulnerable to timing attacks, for example.

Knighthead answered 26/9, 2017 at 14:0 Comment(1)
Yes, this should work. But somehow I think I am on the wrong track if I solve it like this. I have personal guidelines (for me). One is "Do not write source code to implement a backup" and the next "Do not write source code to implement authentication". This has been solved by more talented people before and there are already implementations which got tested since several months. Nevertheless thank you for this snippet. I though implementing this is much more code.Weems
E
13

You can try a custom decorator (as seems to be the recommended way here and here) instead of adding new middleware:

my_app/decorators.py:

import base64

from django.http import HttpResponse
from django.contrib.auth import authenticate
from django.conf import settings


def basicauth(view):
    def wrap(request, *args, **kwargs):
        if 'HTTP_AUTHORIZATION' in request.META:
            auth = request.META['HTTP_AUTHORIZATION'].split()
            if len(auth) == 2:
                if auth[0].lower() == "basic":
                    uname, passwd = base64.b64decode(auth[1]).decode(
                        "utf8"
                    ).split(':', 1)
                    user = authenticate(username=uname, password=passwd)
                    if user is not None and user.is_active:
                        request.user = user
                        return view(request, *args, **kwargs)
        
        response = HttpResponse()
        response.status_code = 401
        response['WWW-Authenticate'] = 'Basic realm="{}"'.format(
            settings.BASIC_AUTH_REALM
        )
        return response
    return wrap

Then use this to decorate your view:

from my_app.decorators import basicauth


@basicauth
def my_view(request):
    ...
Extrados answered 20/12, 2017 at 9:23 Comment(2)
There seems to be a mistake in the code example above. It should read uname, passwd = base64.b64decode(auth[1]).decode("UTF8").split(':') The result of base64 in Python 3 is byte object and UTF8 is the default encoding using today.Traitorous
.split(':') should be .split(':', 1) to limit how many times the string is split in case a colon : is in the username/password.Pufahl
W
7

This library could be used: https://github.com/hirokiky/django-basicauth

Basic auth utilities for Django.

The docs show how to use it:

Applying decorator to CBVs

To apply @basic_auth_requried decorator to Class Based Views, use django.utils.decorators.method_decorator.

Source: https://github.com/hirokiky/django-basicauth#applying-decorator-to-cbvs

Weems answered 13/10, 2017 at 13:49 Comment(1)
how do I apply it to admin site?Sirdar
A
2

For those that already use django-rest-framework (DRF):

DRF has a BasicAuthentication class which, more-or-less, does what is described in the other answers (see source).

This class can also be used in "normal" Django views.

For example:

from rest_framework.authentication import BasicAuthentication

def my_view(request):
    # use django-rest-framework's basic authentication to get user
    user = None
    user_auth_tuple = BasicAuthentication().authenticate(request)
    if user_auth_tuple is not None:
        user, _ = user_auth_tuple
Aphis answered 14/4, 2020 at 20:31 Comment(0)
D
0

Expanding on the answer from djvg and adopting the wrapper from John Moutafis to use with django-rest-framework / DRF - this allow you to wrap a single endpoint with a static user/pass (for example if you want to quickly have a documentation endpoint available with a static user/password without setting up regular users in your django application):

import base64
from django.http import HttpResponse

def wrap_with_static_basicauth(view, expected_username, expected_password):
    def wrap(request, *args, **kwargs):
        if 'HTTP_AUTHORIZATION' in request.META:
            auth = request.META['HTTP_AUTHORIZATION'].split()
            if len(auth) == 2:
                if auth[0].lower() == "basic":
                    uname, passwd = base64.b64decode(auth[1]).decode(
                        "utf8"
                    ).split(':', 1)

                    if uname == expected_username and passwd == expected_password:
                        return view(request, *args, **kwargs)

        response = HttpResponse()
        response.status_code = 401
        response['WWW-Authenticate'] = 'Basic realm="Service name"'
        return response

    return wrap

You can then use this as a decorator, but since you'd usually use .as_view() with DRF's endpoints, you can wrap it manually as well (useful if you can't control the view code itself because it's in drf-spectacular for example):

authenticated_schema = wrap_with_static_basicauth(SpectacularAPIView.as_view(),
                                                  settings.DOCS_AUTHENTICATION['USER'],
                                                  settings.DOCS_AUTHENTICATION['PASSWORD'],
                                                  )

You can then configure the authentication details by defining DOCS_AUTHENTICATION in your settings.

Damron answered 14/4, 2023 at 8:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.