Django: Handling Expired API Keys Without Signals

Title: Django: Handling Expired API Keys Without Signals

Question: How can I implement a logic in Django to automatically delete expired API keys from the OutstandingKey model and update the corresponding BlacklistedKey entry when the expiry date has passed, without using signals?

I have two Django models: OutstandingKey and BlacklistedKey. The OutstandingKey model represents API keys generated by users, including an expiry date field. The BlacklistedKey model stores blacklisted API keys, referencing the OutstandingKey through a one-to-one relationship.

I want to create a system where expired API keys are automatically removed from the OutstandingKey model and added to the BlacklistedKey model when their expiry date has passed. However, I prefer not to use signals for this implementation due to this Django Anti-Pattern.

Could someone provide a blueprint or example of how to achieve this logic within Django?.

Below is my Models

from django.db import models
from django.conf import settings
from django.utils import timezone

class OutstandingKey(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        default=None,
        verbose_name="User",
    )
    name = models.CharField(max_length=1000, default="", blank=True, null=True)
    api_key = models.CharField(
        max_length=1000,
        unique=True,
        verbose_name="API Key",
        default="",
        blank=True,
        null=True,
        editable=False,
    )
    expiry_date = models.DateTimeField(
        default=None, verbose_name="Expiry Date", blank=True, null=True
    )
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name_plural = "Outstanding Tokens"
        verbose_name = "Outstanding API Key"
        ordering = ["-timestamp"]


class BlacklistedKey(models.Model):
    key = models.OneToOneField(
        OutstandingKey, on_delete=models.CASCADE, primary_key=True
    )
    key_value = models.CharField(
        max_length=1000,
        unique=True,
        verbose_name="API Key",
        default="",
        blank=True,
        null=True,
    )
    blacklisted_at = models.DateTimeField(
        auto_now_add=True, verbose_name="Blacklisted At"
    )

    class Meta:
        verbose_name_plural = "Blacklisted Tokens"
        verbose_name = "Blacklisted Key"

    def __str__(self):
        return self.key.name

This is not something that should be done within your Django web process.

You should do this either as a Celery task scheduled by Celery beat, or as a custom management command run as a cron job.

You don’t really need to move the expired keys when they expire, you should simply need to check if the key exists and is not expired.

class ApiKeyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        api_key = None
        try:
            api_key = OutstandingKeys.objects.get(api_key=request.headers.get('bla bla', None), expiry_date__gt=datetime.datetime.now(...))
        except OutstandingKkeys.DoesNotExist:
            # no valid key found
            return HttpResponse(status=403)

        response = self.get_response(request)
        return response

However, this means you will be left with a bunch of expired keys in the OutstandingKeys table (btw, it is a good idea to change the model name to ActiveKeys or ApiKeys even, outstanding means something that still needs to happen or something out of the ordinary). To clean them up, you could either have a scheduled task of sorts, say with Celery, or use the request_finished signal handler (ha!).

OutstandingKeys.objects.filter(expiry_date__lt=datetime.datetime.now(...)).delete()

(note the difference in the expiry_date clause).

If you need to ban a key, say it was leaked, you simply delete it from the table.

There are no reasons to have 2 tables.

There is, if you have a “subscription” type situation and you want to let people know that their API key has expired and needs to be renewed. This is a different case than just rejecting an API key because it doesn’t exist.

(Note: I have no idea whether the original poster is in this situation or not. I am only pointing out that there are situations where it is of value to track expired or blacklisted keys.)

I sort of disagree. Is not very kind to your users if you notify them after the key expired, but a few units of time before (my council does that, “your council tax is 1 month overdue bla bla”, why didn’t you tell me before it was overdue…). You could add a “blacklistend” bool field in the ApiKeys table to track the blacklisted keys and simply not delete the expired keys. See, no need for 2 tables :slightly_smiling_face:

In our use-case, subscriptions aren’t consistent. Someone may use a service one month a year. There’s no need for them to pay for a full year. And reminding them in January for a bill not due until December is rarely helpful.

The next year comes around and they try to use their app. They then get a nice notice that they haven’t restarted their subscription…

Yes, there are a lot of cases where you can do things in one table instead of multiple tables. However, we have made the architectural decision to not overload table functionality. We’d rather go with 10 tables each having a discrete and definable purpose than to have 1 table with conditionals to determine state, when the functionality using those tables doesn’t overlap.

I think we are deviating from the topic at hand and funnily enough we do agree on the basic answer to the OP’s question: scheduled task of sorts.

Please note that I am not arguing that using only one table is the way to go for all cases, in the end I am a happy polymorphic models user. Just in this particular use case…

I agree on all counts. I don’t consider this an “argument” at all - this is (to me) a friendly discussion of alternative perspectives.

Cheers!

1 Like