DEV Community

elite_developer
elite_developer

Posted on

How to implement custom Api key authorization in your django application.

To use a custom database model as an HTTP header token, the database model itself does not automatically read request headers. In a web application framework like Django REST Framework (DRF), the configuration is split into two distinct responsibilities:

1. The Database Model: Stores and validates the API key (ideally stored as a secure hash rather than plain text).

2. The Authentication Class / Middleware: Reads incoming HTTP headers (such as Authorization: Api-Key <token> or X-Api-Key: <token>), extracts the value, hashes it, and queries the database model.

Here is how you set up both components and why each piece works the way it does.

Step 1: Define the Database Model
When creating the model, we store a hashed key rather than raw plaintext so that a database breach does not leak raw tokens. We also store a prefix or mask to identify the key in UI logs.

import secrets
import hashlib
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class APIKey(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_keys")
    name = models.CharField(max_length=50)
    key_hash = models.CharField(max_length=128, unique=True, editable=False)
    prefix = models.CharField(max_length=8, editable=False)
    created_at = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)

    @classmethod
    def generate_key(cls, user, name):
        """
        Generates a new API key.
        Returns a tuple: (APIKey instance, raw_key string)
        """
        # Generate a cryptographically secure random token
        raw_key = f"sk_live_{secrets.token_urlsafe(32)}"
        prefix = raw_key[:8]

        # Hash the token with SHA-256 before saving
        key_hash = hashlib.sha256(raw_key.encode()).hexdigest()

        api_key_obj = cls.objects.create(
            user=user,
            name=name,
            prefix=prefix,
            key_hash=key_hash
        )

        # Return raw_key ONLY ONCE to show the user
        return api_key_obj, raw_key

    def __str__(self):
        return f"{self.name} ({self.prefix}...)"

Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the Header Extraction Authentication Class
To hook this model into incoming requests, we write a custom authentication class that inherits from rest_framework.authentication.BaseAuthentication.

This class intercepts incoming headers, extracts the token, and matches its hash against APIKey.key_hash.

import hashlib
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .models import APIKey

class APIKeyHeaderAuthentication(BaseAuthentication):
    """
    Custom authentication class that parses the API key from 
    either the 'X-Api-Key' header or the standard 'Authorization' header.
    """
    def authenticate(self, request):
        # 1. Look for custom header 'X-Api-Key'
        api_key = request.META.get('HTTP_X_API_KEY')

        # 2. Fallback: Look for 'Authorization: Api-Key <token>'
        if not api_key:
            auth_header = request.META.get('HTTP_AUTHORIZATION')
            if auth_header and auth_header.startswith('Api-Key '):
                api_key = auth_header.split(' ')[1]

        # If no key in header, return None to allow unauthenticated or next auth class
        if not api_key:
            return None

        # 3. Hash the incoming key to match the database stored hash
        hashed_input_key = hashlib.sha256(api_key.encode()).hexdigest()

        # 4. Query database for matching active key
        try:
            key_obj = APIKey.objects.select_related('user').get(
                key_hash=hashed_input_key, 
                is_active=True
            )
        except APIKey.DoesNotExist:
            raise AuthenticationFailed('Invalid or inactive API key.')

        # 5. Return (user, auth) tuple to set request.user and request.auth
        return (key_obj.user, key_obj)

    def authenticate_header(self, request):
        """
        Returns a string to be used as the value of the WWW-Authenticate
        header in a 401 Unauthenticated response.
        """
        return 'Api-Key realm="API"'

Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Views or Global Settings
You can apply this authentication strategy globally in your settings.py or per view.

Option A: Per View Basis

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .authentication import APIKeyHeaderAuthentication

class SecureDataView(APIView):
    authentication_classes = [APIKeyHeaderAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({
            "message": "Authenticated successfully!",
            "user": request.user.username,
            "key_prefix": request.auth.prefix
        })

Enter fullscreen mode Exit fullscreen mode

Option B: Global REST Framework Settings (settings.py)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'your_app.authentication.APIKeyHeaderAuthentication',
        'rest_framework.authentication.SessionAuthentication', # Optional fallback
    ],
}

Enter fullscreen mode Exit fullscreen mode

How the Request Header Flow Works
When sending HTTP requests from a client (Postman, Frontend, Curl), send the key using one of the configured headers:

bash 

# Using custom header
curl -H "X-Api-Key: sk_live_your_raw_token_here" https://api.yourdomain.com/secure-data/

# OR using Authorization header
curl -H "Authorization: Api-Key sk_live_your_raw_token_here" https://api.yourdomain.com/secure-data/
Enter fullscreen mode Exit fullscreen mode

Top comments (0)