everspader
everspader

Reputation: 1700

How to write custom authentication backend for one endpoint only (/metrics) in Django?

I have a custom middleware in Django to force all the requests to go through a login authentication (with few exceptions like api/token).

This project allows users to authenticate either via a JWT token or a login in /admin/login and all unauthenticated users are redirected to /admin/login. for authentication.

We deployed the project in Kubernetes and we want Prometheus to scrape /metrics endpoint but we don't want it to be exposed to unauthenticated users. Prometheus allows for authentication with username and password. The thing is that when a request is sent to /metrics, because of the middleware, the request is redirected to /admin/login.

So I believe I need to write a custom authentication backend specifically designed for the metrics endpoint and place it before the other authentication methods.

The request always goes through the middleware first so it will always be redirected to /admin/login and then will go through the authentication backend.

What is the right way of doing this?

class LoginRequiredMiddleware(MiddlewareMixin):
    def __init__(self, get_response):
        self.get_response = get_response

    def process_request(self, request):
        assert hasattr(request, 'user')

        path = request.path_info.lstrip('/')

        if path == '' or path == '/':
            return self.get_response(request)

        url_is_exempt = any(url.match(path) for url in EXEMPT_URLS)

        if request.user.is_authenticated or url_is_exempt:
            # If the user is authenticated OR the URL is in the exempt list
            # go to the requested page
            return self.get_response(request)

        else:
            # Trying to access any page as a non authenticated user
            return redirect(f"{settings.LOGIN_URL}?next=/{path}")
class MetricsAuthBackend(BaseBackend):

    def authenticate(self, request, username=None, password=None):
        if '/metrics' in request.path:
            if username == "username":
                #need to fix this to use the hash of the password
                pwd_valid = check_password(password, "password")

                if pwd_valid:
                    user = User.objects.get(username=username)
                    return user

        return None

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

Upvotes: 4

Views: 492

Answers (1)

Idris Olokunola
Idris Olokunola

Reputation: 436

lets try this approach

Adjust the middleware to skip redirecting the /metrics endpoint to /admin/login.

Update your LoginRequiredMiddleware to bypass /metrics authentication checks for Promitheus:

(middleware.py)

from django.conf import settings
from django.shortcuts import redirect
from django.utils.deprecation import MiddlewareMixin
import re

class LoginRequiredMiddleware(MiddlewareMixin):
    EXEMPT_URLS = [re.compile(settings.LOGIN_URL.lstrip('/'))]
    
    def __init__(self, get_response):
        self.get_response = get_response

    def process_request(self, request):
        path = request.path_info.lstrip('/')

        #Bypass the /metrics endpoint to use custom authentication
        if path == 'metrics':
            return None

        # Check for authentication or exempt URLs
        if request.user.is_authenticated or any(url.match(path) for url in self.EXEMPT_URLS):
            return self.get_response(request)
        
        return redirect(f"{settings.LOGIN_URL}?next=/{path}")

Create a custom authentication backend for /metrics that checks against a username & password, and returns an authenticated user if valid.

create another file called backend.py(you can name it whatever you like)

(backends.py)

from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import check_password

User = get_user_model()

class MetricsAuthBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None):
        # Only apply this authentication to /metrics endpoint
        if request.path == '/metrics':
            metrics_username = "username"  #replace with your metrics username
            metrics_password_hash = "hashed_password"  # replace with hashed password
            
            if username == metrics_username and check_password(password, metrics_password_hash):
                try:
                    return User.objects.get(username=username)
                except User.DoesNotExist:
                    return None
        return None

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

In settings.py, make sure this custom backend is included as an authentication method so it can be recognised:

(settings.py)

AUTHENTICATION_BACKENDS = [
    'path.to.backends.MetricsAuthBackend',
    'django.contrib.auth.backends.ModelBackend',  # default backend
]

LOGIN_URL = '/admin/login/'

Upvotes: -1

Related Questions