How to delete a django JWT token? - python

I am using the Django rest framework JSON Web token API that is found here on github (https://github.com/GetBlimp/django-rest-framework-jwt/tree/master/).
I can successfully create tokens and use them to call protected REST APis. However, there are certain cases where I would like to delete a specific token before its expiry time. So I thought to do this with a view like:
class Logout(APIView):
permission_classes = (IsAuthenticated, )
authentication_classes = (JSONWebTokenAuthentication, )
def post(self, request):
# simply delete the token to force a login
request.auth.delete() # This will not work
return Response(status=status.HTTP_200_OK)
The request.auth is simply a string object. So, this is of course, not going to work but I was not sure how I can clear the underlying token.
EDIT
Reading more about this, it seems that I do not need to do anything as nothing is ever stored on the server side with JWT. So just closing the application and regenerating the token on the next login is enough. Is that correct?

The biggest disadvantage of JWT is that because the server does not save the session state, it is not possible to abolish a token or change the token's permissions during use. That is, once the JWT is signed, it will remain in effect until it expires, unless the server deploys additional logic.
So, you cannot invalidate the token even you create a new token or refresh it. Simply way to logout is remove the token from the client.

Yes, it's correct to say that JWT tokens are not stored in the database. What you want, though, is to invalidate a token based on user activity, which doesn't seem to be possible ATM.
So, you can do what you suggested in your question, or redirect the user to some token refreshing endpoint, or even manually create a new token.

Add this in Admin.py
class OutstandingTokenAdmin(token_blacklist.admin.OutstandingTokenAdmin):
def has_delete_permission(self, *args, **kwargs):
return True # or whatever logic you want
admin.site.unregister(token_blacklist.models.OutstandingToken)
admin.site.register(token_blacklist.models.OutstandingToken, OutstandingTokenAdmin)

from rest_framework_simplejwt.token_blacklist.admin import OutstandingTokenAdmin
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
class OutstandingTokenAdmin(OutstandingTokenAdmin):
def has_delete_permission(self, *args, **kwargs):
return True # or whatever logic you want
def get_actions(self, request):
actions = super(OutstandingTokenAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
admin.site.unregister(OutstandingToken)
admin.site.register(OutstandingToken, OutstandingTokenAdmin)

Related

Django Rest Framework gives 302 in Unit tests when force_login() on detail view?

I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.

User Authentication taking a long time in DRF

I'm using a ModelViewSet with the IsAuthenticatedOrReadOnly permission class, like so:
class PostViewSet(viewsets.ModelViewSet, MarkdownToHTML):
permission_classes = (IsAuthenticatedOrReadOnly,)
When I call this view in the browsable API, the data returns in about 1100 ms (already too long), but when I call it from my frontend UI, the call takes 6000-7000ms!
The only difference between these two methods of calling the same view is that I am passing along a json token from my frontend UI app. When I comment out the token header, the response returns in about 1 second, the same time as in the browsable API.
How could this simple authentication step take over 5 seconds?
Here is the permission class:
class IsAuthenticatedOrReadOnly(BasePermission):
"""
The request is authenticated as a user, or is a read-only request.
"""
def has_permission(self, request, view):
return (
request.method in SAFE_METHODS or
request.user and
request.user.is_authenticated
)
I've run in a similar issue in a project. I would tell you about my experience in order to try to help, I can't tell what is your exact problem but I'll post things I checked when I had mine.
The thing is that decoding the auth token is a very expensive operation, so you have to check:
How many times is such token (if provided) decoded in your view?
Can you use cookies for caching auth token, setting an expire time?
How many time is this token send to and back from the server?
On the other hand, remember DRF will transform you json to a python object (specifically a dictionary) depending on the length of your token (and how many times it occurs) it will be a very expensive operation too.

Require new login for each page view

I am currently using code found here:
http://flask.pocoo.org/snippets/8/
And I decorate my function accordingly to have the admin authenticate when requesting a specific admin page. However, instead of requiring the admin to keep authenticating each time they admin page, I noticed that it somehow keeps track of the session and no longer requires authentication after successfully authenticating once. Is there some way to force flask to re-authenticate every time an admin requests the given decorated admin page?
Using the included snippet, there is no good way to force a user to log in every time they request the given page.
This is because that snippet is using HTTP Basic Auth and there is no good way to ask the browser to stop sending that header.
What you are looking for can be done with a custom decorator. You can use the sample below. Note that your case will be different, but you can use this as a guide.
from web import app, has_role
#app.route("/admin/my_page")
#login_required
#has_role(role="admin")
def admin_my_page():
//do stuff
Then, in your project init, or an include file you can add the following:
def has_role(role=None):
def _initial_decorator(view_func):
def _decorator(*args, **kwargs):
response = view_func(*args, **kwargs)
if g.user.user_level != role:
from flask import redirect, url_for
return redirect(url_for("no_access"))
return response
return wraps(view_func)(_decorator)
return _initial_decorator
This should at lease give you an idea of how to create a custom decorator, and then check for role permissions. You can expand this to however you need. You can put engine logic, or other checks to fit your project.

Create Django token-based Authentication [duplicate]

I am trying to figure out the best way to implement token based authentication in my django app. An external, non-django application is setting a cookie, with a token, and I have a webservice that can retrieve user information based off of that token. If the user has the cookie set, they should not need to authenticate on my site and should be automatically logged in based on the info passed back by the web service. As I see it, there are a few different options to perform the actual check and I'm not sure which is best:
Write a custom decorator like the one in this snippet and use it instead of
login_required.
Call a custom authenticate method inside base_site through an ajax call. On every page, a check would be made and if the cookie exists and is valid, then the user would be automatically logged in.
Add some javascript to the LOGIN_REDIRECT_URL page that will check/validate the cookie in an ajax call, and automatically redirect back to the referrer if the cookie authenticated.
Is there an option I am missing? Ideally, there would be a way to build this into login_required, without having to write a custom decorator.
Before searching for code, be sure you read the documentation. http://docs.djangoproject.com/en/1.2/topics/auth/#other-authentication-sources
Also read the supplied Django source.
You want to create three things.
Middleware to capture the token. This is where most of the work happens. It checks for the token, authenticates it (by confirming it with the identity manager) and then logs in the user.
Authentication backend to find Users. This is a stub. All it does is create users as needed. Your identity manager has the details. You're just caching the current version of the user on Django's local DB.
Here's the middleware (edited).
from django.contrib.auth import authenticate, login
class CookieMiddleware( object ):
"""Authentication Middleware for OpenAM using a cookie with a token.
Backend will get user.
"""
def process_request(self, request):
if not hasattr(request, 'user'):
raise ImproperlyConfigured()
if "thecookiename" not in request.COOKIES:
return
token= request.COOKIES["thecookiename"]
# REST request to OpenAM server for user attributes.
token, attribute, role = identity_manager.get_attributes( token )
user = authenticate(remote_user=attribute['uid'][0])
request.user = user
login(request, user)
The identity_manager.get_attributes is a separate class we wrote to validate the token and get details on the user from the IM source. This, of course, has to be mocked for testing purposes.
Here's a backend (edited)
class Backend( RemoteUserBackend ):
def authenticate(**credentials):
"""We could authenticate the token by checking with OpenAM
Server. We don't do that here, instead we trust the middleware to do it.
"""
try:
user= User.objects.get(username=credentials['remote_user'])
except User.DoesNotExist:
user= User.objects.create(username=credentials['remote_user'] )
# Here is a good place to map roles to Django Group instances or other features.
return user
This does not materially change the decorators for authentication or authorization.
To make sure of this, we actually refresh the User and Group information from our
identity manager.
Note that the middleware runs for every single request. Sometimes, it's okay to pass the token to the backed authenticate method. If the token exists in the local user DB, the request can proceed without contacting the identity manager.
We, however, have complex rules and timeouts in the identity manager, so we have to examine every token to be sure it's valid. Once the middleware is sure the token is valid, we can then allow the backend to do any additional processing.
This isn't our live code (it's a little too complex to make a good example.)

Django user impersonation by admin

I have a Django app. When logged in as an admin user, I want to be able to pass a secret parameter in the URL and have the whole site behave as if I were another user.
Let's say I have the URL /my-profile/ which shows the currently logged in user's profile. I want to be able to do something like /my-profile/?__user_id=123 and have the underlying view believe that I am actually the user with ID 123 (thus render that user's profile).
Why do I want that?
Simply because it's much easier to reproduce certain bugs that only appear in a single user's account.
My questions:
What would be the easiest way to implement something like this?
Is there any security concern I should have in mind when doing this? Note that I (obviously) only want to have this feature for admin users, and our admin users have full access to the source code, database, etc. anyway, so it's not really a "backdoor"; it just makes it easier to access a user's account.
I don't have enough reputation to edit or reply yet (I think), but I found that although ionaut's solution worked in simple cases, a more robust solution for me was to use a session variable. That way, even AJAX requests are served correctly without modifying the request URL to include a GET impersonation parameter.
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = int(request.GET["__impersonate"])
elif "__unimpersonate" in request.GET:
del request.session['impersonate_id']
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(id=request.session['impersonate_id'])
Usage:
log in: http://localhost/?__impersonate=[USERID]
log out (back to admin): http://localhost/?__unimpersonate=True
It looks like quite a few other people have had this problem and have written re-usable apps to do this and at least some are listed on the django packages page for user switching. The most active at time of writing appear to be:
django-hijack puts a "hijack" button in the user list in the admin, along with a bit at the top of page for while you've hijacked an account.
impostor means you can login with username "me as other" and your own password
django-impersonate sets up URLs to start impersonating a user, stop, search etc
I solved this with a simple middleware. It also handles redirects (that is, the GET parameter is preserved during a redirect). Here it is:
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.user = models.User.objects.get(id=int(request.GET["__impersonate"]))
def process_response(self, request, response):
if request.user.is_superuser and "__impersonate" in request.GET:
if isinstance(response, http.HttpResponseRedirect):
location = response["Location"]
if "?" in location:
location += "&"
else:
location += "?"
location += "__impersonate=%s" % request.GET["__impersonate"]
response["Location"] = location
return response
#Charles Offenbacher's answer is great for impersonating users who are not being authenticated via tokens. However, it will not work with clients side apps that use token authentication. To get user impersonation to work with apps using tokens, one has to directly set the HTTP_AUTHORIZATION header in the Impersonate Middleware. My answer basically plagiarizes Charles's answer and adds lines for manually setting said header.
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = int(request.GET["__impersonate"])
elif "__unimpersonate" in request.GET:
del request.session['impersonate_id']
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(id=request.session['impersonate_id'])
# retrieve user's token
token = Token.objects.get(user=request.user)
# manually set authorization header to user's token as it will be set to that of the admin's (assuming the admin has one, of course)
request.META['HTTP_AUTHORIZATION'] = 'Token {0}'.format(token.key)
i don't see how that is a security hole any more than using su - someuser as root on a a unix machine. root or an django-admin with root/admin access to the database can fake anything if he/she wants to. the risk is only in the django-admin account being cracked at which point the cracker could hide tracks by becoming another user and then faking actions as the user.
yes, it may be called a backdoor, but as ibz says, admins have access to the database anyways. being able to make changes to the database in that light is also a backdoor.
Set up so you have two different host names to the same server. If you are doing it locally, you can connect with 127.0.0.1, or localhost, for example. Your browser will see this as three different sites, and you can be logged in with different users. The same works for your site.
So in addition to www.mysite.com you can set up test.mysite.com, and log in with the user there. I often set up sites (with Plone) so I have both www.mysite.com and admin.mysite.com, and only allow access to the admin pages from there, meaning I can log in to the normal site with the username that has the problems.

Categories

Resources