Djangorestframework-simplejwt: Validate without database query

Created on 16 Jun 2020  路  10Comments  路  Source: jazzband/djangorestframework-simplejwt

For me the attractive feature of JWT is the ability to provide validation of a request without the need to make a database query.

However, in the current implementation here, it appears that the related User model is queried and populated on every request.

https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/master/rest_framework_simplejwt/authentication.py#L100-L117

Is it feasible to remove this query, or is the User object required by the underlying django-rest-framework?

question

All 10 comments

@monkut You can use the "experimental feature" found here: https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/master/rest_framework_simplejwt/authentication.py#L120

Yes, the default implementation gives you the user instance since an object is given instead of a QuerySet. Perhaps we should change that functionality to instead be a filter() which would mean lazy evaluation.

Anyways, using this authentication backend works the same way except you won't be able to get the user instance. You can read up the security evaluation here: https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/master/rest_framework_simplejwt/models.py#L18

Basically, since the refresh token issued in TokenObtainPairView forces an authentication but the access token doesn't, you can use the JWT package to validate that 1. the token hasn't expired and 2. the token is valid and you can extract the payload.

That's what I do for my websocket applications.

Yes, I realized after I posted this that .get() will return a queryset, which should be lazy...but then wasn't sure if drf uses it in anyway that would instantiate or result in a db hit...

Thanks, I'll take a look at the feature you mention and give it a try!

Err incorrect. I'm pretty sure .get() does not return a QuerySet but a regular object/instance, which your initial point was correct about. The authenticate DOES hit the DB and WILL get the entire object.

Here's what you should probably do:

You should extend the TokenUser model that you can find in models.py in the package. Add a method that will use the ID of your TokenUser to perform a lookup query IF request.user in your DRF view is accessed.

If request.user only needs the id (e.g. request.user.id), then the evaluation of the query is not performed, and you saved yourself a DB hit. If you need other attributes, then you should first lookup what the other attributes are by importing the model, check that the attribute/column exists, and then perform the .get() DB hit.

@monuk Hope that helps! If possible, I'd like to write a PR for that at some point in time. It'll save everyone's time, but it's a new feature, and I wonder how we can integrate this seamlessly so that no code needs to be changed...

Also, let me know if that made ANY sense. I just came up with that on the spot, because I too, even though I'm a triager, didn't even think to clearly about the authentication backend and that sly description about not hitting the DB haha.

Lastly, with that "experimental feature", note that we use cached_property in so to save some DB calls. It might be helpful to extend that TokenUser model in models.py to your own attributes. It may help to view our test cases to better understand this.

Thanks, again! I'll read up some more and see if I can put some time into this.

I did start to look into SimpleLazyObject here and was wondering if this could be an easy drop in for the current .get() call.

Anyway, still needs some more investigation...

Ok, so using JWTTokenUserAuthentication and setting any related user foreignkey values using the _id field seems to resolve this issue.

It would be nice to make the standard JWTAuthentication use a lazy object, so that it's only retrieved when needed, but at least using JWTTokenUserAuthentication provides a nice workaround.

Related settings.py django config:
```
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTTokenUserAuthentication",),
}

As a side note, instead of changing the default behavior, you can selectively change the authentication_classes in the view-sets that don't require the full user info:

from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication

class ExampleViewSet(viewsets.ModelViewSet):
    authentication_classes = [JWTTokenUserAuthentication]  # <- don't fetch user from DB
    # ...etc..

@monkut The problem with changing it to a SimpleLazyObject is that it'd still be used for the authentication method, so we're still grabbing the entire object. To make it faster, you can have a user model that doesn't change that much with ID field of BigAutoField and use Django-cachalot. It'll make authentication much more smooth, but you don't need to think about optimizations if you're just getting started. It won't matter if you only have a million users tbh.

I just want to mention one more thing: https://code.djangoproject.com/ticket/23011

In this scenario, Bertrand suggested just grabbing the user ID from the session cookie to avoid constantly querying the database. But the security implication is simple: what if it's spoofed?

Closing as not only stale but potentially dangerous.

Was this page helpful?
0 / 5 - 0 ratings