I'm working on a Django/DRF app, and I'm trying to implement an API throttle that will have an increasingly-long delay for failed login attempts.
Eg. lock the user out for 1 minute after 3 failed attempts, 10 minutes after 6 fails, 30 minutes after 9, etc., similar to what phones do and to what is fairly common to login pages in general. I was surprised to find that there doesn't appear to be a progressive throttle already built in to Django or DRF given how common that login scenario is...
DRF Throttle option:
The Django Rest Framework APIView provides a throttle_classes field & a get_throttles() method, and it has a handful of general throttles for doing a fixed-rate throttle delay. I can kind-of mimic a progressive rate by adding a list of throttles, like so:
def get_throttles(self):
return [
MyCustomThrottle('3/m'),
MyCustomThrottle('6/10m'),
MyCustomThrottle('9/30m'),
]
and then add a custom get_cache_key() method to MyCustomThrottle that returns a unique key that doesn't collide with the other throttles in the list.
That almost works - it works for blocking a bot that just has its foot on the gas - however, it has a couple of problems:
DRF throttles don't have an easy way of clearing the throttle list if/when the user logs in successfully. I've kind-of hacked my way around that by manually modifying the cache that the DRF throttles use, but that's not ideal...
DRF throttles are triggered at a certain point in the request loop, and authentication may or may not have happened by that point - so the throttles may not know if the incoming credentials are good:
A. If doing authentication via the
APIView.authentication_classesfield, then auth happens before throttles, then the throttles can know if auth was successful & can act accordingly. That has the downside that every bot request will result in a DB hit.B. If doing auth in the view code, then auth happens after throttles are triggered. The downside there is that throttles don't know if the incoming creds are good, but the upside is that bots are blocked before the DB is hit.
Our app is doing option B since we're also implementing 2FA (maybe there's a way to do 2FA via authentication_classes, but it is what it is today...) and because we want bots to be blocked with minimal DB pummeling. Option B kinda rules out DRF throttles since the edge cases cause a crappy/confusing UX.
Other options:
I've started looking at django-axes as an alternative to DRF throttles.
Upsides:
- It appears to be more auth-focussed & built from the ground up with auth in mind. It implements throttling via middleware, so it is auth-friendly regardless of where/when auth happens.
- It provides an easy way to clear throttles on a user, IP, or general-basis.
Downsides:
- It doesn't appear to have a way to provide multiple throttles for a progressive/increasing throttle delay like I'm shooting for.
- It's mostly designed as a "lock the user out until manually cleared" library. It does have an option to provide a "cooloff" time to have it automatically unlocked, but it doesn't report, to the user, how much cooloff time is left (like DRF does), nor does it provide an easy way for me to provide that info to the user (maybe it does - I'm still kinda new with this library).
- It appears to have just one set of config options for the entire app - no view-specific config. We have 2 login pages for 2 types of users. Each has different throttling needs.
Maybe there's a way I can make django-axes work but, regardless, I feel like I'm re-inventing the wheel here... Progressive throttling is pretty common in the login world, but it seems to be completely absent in the Django world...
Is there something simple that I'm missing? It seems that something like this should already be built into Django/DRF somewhere...