Django Rest Framework request header based permission - django

I'm building a Django app with Django Rest Framework to host it on my organisation's domain. The domain implements a custom authentication protocol. When someone accesses the domain, say to app1.domainname.com, they are redirected to the organisation's login page (login.domainname.com) and they have to log in with their staff account. After the user is authenticated, the user is redirected back to their initial destination (app1.domain.com). The information of the user is then stored in some custom header fields of the HTTP request sent to the app. E.g.
GET / HTTP/2
Content-Type:
User-Agent: ...
...
X-Username: johndoe1
X-Firstname: John
X-Lastname: Doe
X-Email: johndoe#domainname.com
etc.
I'm trying to implement custom permission for my REST API that looks for these fields in the headers, and then authorise the user based on their user information. This is what I'm currently having:
from rest_framework.permissions import BasePermission
allowed = ['johndoe1', 'dicksmith2', 'username3']
class CutomPerm(BasePermission):
message = "You don't have permission to access this object"
def has_object_permission(self, request, view, obj):
print(request.headers)
username = request.headers['X-Username']
return username in allowed
But when I run the server, it seems like the custom headers are passed through to the backend. For some requests they are, but ultimately the user is not authorised because the has_object_permission method raises a KeyError:
[10/Mar/2020 10:03:29] "GET /api/obj/ HTTP/1.1" 200 81
[10/Mar/2020 10:03:29] "GET /favicon.ico/ HTTP/1.1" 200 11
{'Content-Length': '', 'Content-Type': 'text/plain', 'Host': 'localhost:8000', 'Connection': 'keep-alive', etc., 'X-Username': 'johndoe1', 'X-Firstname': 'John', etc.}
Forbidden: /api/obj/1/
[10/Mar/2020 10:04:35] "GET /api/obj/1/ HTTP/1.1" 403 6581
{'Content-Length': '', 'Content-Type': 'text/plain', 'Host': 'localhost:8000', 'Connection': 'keep-alive', etc.} # no custom headers here
[10/Mar/2020 10:04:35] "GET /favicon.ico/ HTTP/1.1" 200 11
Internal Server Error: /api/obj/1/
Traceback (most recent call last):
File "/path/to/project/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
response = get_response(request)
File "/path/to/project/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "/path/to/project/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/path/to/project/venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/viewsets.py", line 114, in view
return self.dispatch(request, *args, **kwargs)
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/views.py", line 505, in dispatch
response = self.handle_exception(exc)
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/views.py", line 465, in handle_exception
self.raise_uncaught_exception(exc)
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
raise exc
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/views.py", line 502, in dispatch
response = handler(request, *args, **kwargs)
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/mixins.py", line 54, in retrieve
instance = self.get_object()
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/generics.py", line 99, in get_object
self.check_object_permissions(self.request, obj)
File "/path/to/project/venv/lib/python3.8/site-packages/rest_framework/views.py", line 343, in check_object_permissions
if not permission.has_object_permission(request, self, obj):
File "/path/to/project/project/app/permissions.py", line 11, in has_object_permission
username = request.headers['X-Username']
File "/path/to/project/venv/lib/python3.8/site-packages/django/http/request.py", line 388, in __getitem__
return super().__getitem__(key.replace('_', '-'))
File "/path/to/project/venv/lib/python3.8/site-packages/django/utils/datastructures.py", line 320, in __getitem__
return self._store[key.lower()][1]
KeyError: 'X-Username'
Note that in the 2 header dictionaries printed out, the first one has all the custom headers but the second one doesn't.
I think this is because there are some redirecting happening behind the scene and the final request that gets to the rest framework permission check has lost all of its custom headers. Is there anyway to check for permissions based on the custom headers?
Thanks

Django modifies the http header keys.
You have to access the header as:
username = request.META.get('HTTP_X_USERNAME', None)
if username:
# your logic
pass
checkout Django header docs:
https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META

Related

Django RestFramework JWT Token: Get User DoesNotExist error

After a user is deleted, the tokens on the client side are still valid until the time has expired. The issue is django restframwework does not handle a request from a deleted user and causes a 500. How can I prevent this?
aceback (most recent call last):
File "/lib/python3.6/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/lib/python3.6/site-packages/django/core/handlers/base.py", line 179, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/lib/python3.6/site-packages/django/views/generic/base.py", line 70, in view
return self.dispatch(request, *args, **kwargs)
File "/lib/python3.6/site-packages/rest_framework/views.py", line 505, in dispatch
response = self.handle_exception(exc)
File "/lib/python3.6/site-packages/rest_framework/views.py", line 465, in handle_exception
self.raise_uncaught_exception(exc)
File "/lib/python3.6/site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
raise exc
File "/lib/python3.6/site-packages/rest_framework/views.py", line 493, in dispatch
self.initial(request, *args, **kwargs)
File "/lib/python3.6/site-packages/rest_framework/views.py", line 410, in initial
self.perform_authentication(request)
File "/lib/python3.6/site-packages/rest_framework/views.py", line 324, in perform_authentication
request.user
File "/lib/python3.6/site-packages/rest_framework/request.py", line 220, in user
self._authenticate()
File "/lib/python3.6/site-packages/rest_framework/request.py", line 373, in _authenticate
user_auth_tuple = authenticator.authenticate(self)
File "/lib/python3.6/site-packages/rest_framework_jwt/authentication.py", line 33, in authenticate
payload = jwt_decode_handler(jwt_value)
File "/lib/python3.6/site-packages/rest_framework_jwt/utils.py", line 105, in jwt_decode_handler
secret_key = jwt_get_secret_key(unverified_payload)
File "/lib/python3.6/site-packages/rest_framework_jwt/utils.py", line 26, in jwt_get_secret_key
user = User.objects.get(pk=payload.get('user_id'))
File "/lib/python3.6/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/lib/python3.6/site-packages/django/db/models/query.py", line 431, in get
self.model._meta.object_name
From the JWT token, you are decoding it to get the user_id - payload['user_id'].
The error is happening because of User.objects.get(pk=payload.get('user_id')).
Instead of doing a get, you could use a get_object_or_404. Use it like so:
from django.shortcuts import get_object_or_404
payload = jwt_decode_handler(jwt_value)
user = get_object_or_404 (User, pk=payload.get('user_id'))
This raises a 404 error when a user will not be found; and that will be bubbled up through your view and handlers to return a 404 statuscode.
The suggestion by Druhn Bala works but would return a 404 error which isn't ideal for my use case. Instead I came up with one that returns a custom response. ValidationError
from rest_framework.exceptions allows you to send a 400 error with a custom response.
def jwt_decode_handler(token):
options = {
'verify_exp': api_settings.JWT_VERIFY_EXPIRATION,
}
# get user from token, BEFORE verification, to get user secret key
try:
unverified_user = jwt.decode(token, None, False)
except User.DoesNotExist:
raise ValidationError({"errors": ['Oops! Something went wrong, please logout and login back in!']})
secret_key = unverified_user.securitysettings.jwt_secret #my custom way of storing a unique jwt uuid per user.
return jwt.decode(
token,
api_settings.JWT_PUBLIC_KEY or secret_key,
api_settings.JWT_VERIFY,
options=options,
leeway=api_settings.JWT_LEEWAY,
audience=api_settings.JWT_AUDIENCE,
issuer=api_settings.JWT_ISSUER,
algorithms=[api_settings.JWT_ALGORITHM]
)
Lastly we set the custom decode handler as the default in settings.py.
JWT_AUTH = {
'JWT_DECODE_HANDLER':
'registration.decoder.jwt_decode_handler',
...
}

Email Verification Throwing Error If The User Isnt Signed In Before Clicking The Verify Email Link

Im using allauth with email confirmation. If I register a new user, then log in (without being verified), then open the email and follow the link, the user gets successfully verified in the db and can continue using the site.
However, if I register a new user, then log them out, then open the email and follow the link, the user gets successfully verified in the db, but the link throws this error:
NoReverseMatch at /accounts/confirm-email/MzA:1jZWYM:JgeeuPYRC3QnGOPs3L7kzZEFi5M/
Reverse for 'login' not found. 'login' is not a valid view function or pattern name.
So basically, the user has to already be logged in when they click the link in the email for it to work without error, but if they are not logged in and follow the verify link in the email, they get verified in the db but an error is thrown.
PS- this is using normal registration (nothing to do with social accounts).I should also mention that i did copy the email view from the allauth git and put it in my code, but later deleted it (id expect after deleting it it would look in the default place)
Traceback
Traceback (most recent call last):
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
response = get_response(request)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\core\handlers\base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\core\handlers\base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\views\generic\base.py", line 71, in view
return self.dispatch(request, *args, **kwargs)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\views\generic\base.py", line 97, in dispatch
return handler(request, *args, **kwargs)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\allauth\account\views.py", line 301, in post
return redirect(redirect_url)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\shortcuts.py", line 41, in redirect
return redirect_class(resolve_url(to, *args, **kwargs))
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\shortcuts.py", line 131, in resolve_url
return reverse(to, args=args, kwargs=kwargs)
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\urls\base.py", line 87, in reverse
return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\django\urls\resolvers.py", line 677, in _reverse_with_prefix
raise NoReverseMatch(msg)
urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('about/', include('about.urls')),
path('contact_us/', include('contact_us.urls')),
path('enrolment/', include('enrolment.urls')),
path('accounts/', include('allauth.urls')),
path('profile/', include('users.urls')),
path('', include('home.urls')),
path('our_services/', include('our_services.urls')),
]
Thank you.
Following the following line in the traceback:
File "C:\Users\Acer\AppData\Local\Programs\Python\Python38-32\lib\site-packages\allauth\account\views.py", line 301, in post
it leads to this line in django-allauth:
self._setting("EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL",settings.LOGIN_URL)
I guess you have neither the LOGIN_URL nor a EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL set in your settings.py?
Or it can't be found?

Getting "AuthStateMissing ... Session value state missing." when going to callback URI when using AtlassianOAuth2 with Django

I was trying to setup oauth2 authentication in a Django app. Here's my settings:
*other parts ommited*
# AUTH STUFF
AUTHENTICATION_BACKENDS = (
'social_core.backends.atlassian.AtlassianOAuth2',
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_ATLASSIAN_KEY = ' *my atlassian key here* '
SOCIAL_AUTH_ATLASSIAN_KEY_SECRET = ' *my atlassian secret key here* '
LOGIN_URL = '/auth/login/atlassian-oauth2'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
SOCIAL_AUTH_URL_NAMESPACE = 'social'
SESSION_COOKIE_SECURE = False
# i had to do that^, based on what i have read from
# https://stackoverflow.com/questions/37617308/session-value-missing-after-redirect-with-django-python-social-auth
# but it still doesn't work, sadly...
And then here's my view for the login page:
def index(request):
session_id = request.session.session_key
session_id = hashlib.sha256(str(session_id).encode('utf-8')).hexdigest()
auth_url = 'https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=*my_client_id_here*&scope=read%3Ajira-user%20read%3Ajira-work%20manage%3Ajira-project&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fcomplete%2Fatlassian%2F&state=$'+ session_id +'&response_type=code&prompt=consent'
print(auth_url)
context = {
'message': 'You are now authenticated'
if request.user.is_authenticated else 'You are not authenticated',
'auth_url': auth_url
}
return render(request, 'core/home.html', context)
to explain the stuff below --
the url that I used for Authorization grant before was just:
<a href="{% url "social:begin" "* name of backend here *" %}"> which is from the docs https://python-social-auth-docs.readthedocs.io/en/latest/configuration/django.html. It worked for facebook and google for me - but not with atlassian. So I checked the guide for the atlassian oauth2 (https://developer.atlassian.com/cloud/jira/platform/oauth-2-authorization-code-grants-3lo-for-apps/) and it said that I had to use the Jira Platform Rest API Authorization URL. So it worked for me. I was able to access the page where it asked for authorization from an Atlassian user.
When I click accept, Django Gives me an error that says "AuthStateMissing at /complete/atlassian/". The traceback shows that it raise AuthStateMissing(self, 'state'). I read from the Atlassian Guide that I had to have a state that is "a value that is associated with the user you are directing to the authorization URL, e.g., a hash of the user’s session ID", so I took the hash of the cookie of a user, then placed it to the auth_url -- but it still doesn't work.Here's the request information:
Here's the message from the terminal:
[15/May/2019 02:36:13] "GET /home/ HTTP/1.1" 200 1008
Internal Server Error: /complete/atlassian/
Traceback (most recent call last):
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
response = get_response(request)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
response = view_func(request, *args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_django/utils.py", line 49, in wrapper
return func(request, backend, *args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_django/views.py", line 33, in complete
*args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_core/actions.py", line 43, in do_complete
user = backend.complete(user=user, *args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_core/backends/base.py", line 40, in complete
return self.auth_complete(*args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_core/utils.py", line 259, in wrapper
return func(*args, **kwargs)
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_core/backends/oauth.py", line 388, in auth_complete
state = self.validate_state()
File "/home/vasiliy/.virtualenvs/dj_atlassian/lib/python3.7/site-packages/social_core/backends/oauth.py", line 90, in validate_state
raise AuthStateMissing(self, 'state')
social_core.exceptions.AuthStateMissing: Session value state missing.
really hope you guys could help. thanks
in my settings.py file.
SOCIAL_AUTH_REDIRECT_IS_HTTPS = True
My production server uses nginx to redirect HTTP to HTTPS, and this was the cause for the session state to go missing. Good luck-- hope this helps!

django-social auth example app giving ValueError(Missing backend entry)

While using the example app of the django social-auth the user can login to the social accounts but when they are redirected back to our portal it gives a value error Missing backend entry.
Actual error:
[01/Nov/2013 15:49:21] "GET /login/twitter/ HTTP/1.1" 302 0
[01/Nov/2013 15:49:25] "GET /complete/twitter/?oauth_token=ZETJZMsQQhdWzawgrt8xI
9DMEfyCb2N8jpXkVpYfC8&oauth_verifier=zmIXXCAfa2foLolK1v8PIBMQLlrQD6uEYiJ8fwjT14HTTP/1.1" 302 0
Internal Server Error: /login/error/
Traceback (most recent call last):
File "C:\Python27\lib\site-packages\django\core\handlers\base.py", line 115, in get_response
response = callback(request, *callback_args, **callback_kwargs)
File "C:\Python27\lib\site-packages\python_social_auth-0.1.14py2.7.egg\social\apps\django_app\utils.py", line 31, in wrapper
redirect_uri=uri, *args, **kwargs)
File "..\social_auth\views.py", line 19, in load_strategy
return get_strategy(BACKENDS, STRATEGY, STORAGE, *args, **kwargs)
File "C:\Python27\lib\site-packages\python_social_auth-0.1.14-py2.7.egg\social\strategies\utils.py", line 10, in get_strategy
raise ValueError('Missing backend entry')ValueError: Missing backend entry

BaseDeleteView throws AttributeError (render_to_response missing)

I try to implement a view based on BaseDeleteView for a website that acts as a frontend to an REST backend. Both sides communicate over HTTP requests. What I want to achieve is that I send a GET request to an activation URI (send per email after registration). Inside this view I first send a HTTP request to a backend, and then delete the activation object from the database of the frontend. I don't want to have a confirmation page, so DeleteView is not possible.
class ActivationView(BaseDeleteView):
success_url = "/activation/success/"
def get_object(self, queryset=None):
uuid = self.kwargs['uuid']
try:
obj = AccountRegistration.objects.get(uuid=uuid)
except ObjectDoesNotExist:
raise Http404('Registration not found.')
return obj
def delete(self, request, *args, **kwargs):
obj = self.get_obj()
if obj.expire_date < datetime.now():
obj.delete()
raise Http404('Registration expired.')
# send a http request to the backend
t = Transaction('/activate/%s/' % obj.account_name)
t.emit()
# delete the object
obj.delete()
# and redirect the request
return HttpResponseRedirect(self.get_success_url())
My urls.py looks like that:
url(r'^activate/(?P<uuid>\w+)/$',
ActivationView.as_view(), name="account-activate"),
But I get the following error:
Traceback (most recent call last):
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/contrib/staticfiles/handlers.py", line 68, in __call__
return self.application(environ, start_response)
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/core/handlers/wsgi.py", line 272, in __call__
response = self.get_response(request)
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/core/handlers/base.py", line 169, in get_response
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/core/handlers/base.py", line 203, in handle_uncaught_exception
return debug.technical_500_response(request, *exc_info)
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/core/handlers/base.py", line 111, in get_response
response = callback(request, *callback_args, **callback_kwargs)
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/views/generic/base.py", line 47, in view
return self.dispatch(request, *args, **kwargs)
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/views/generic/base.py", line 68, in dispatch
return handler(request, *args, **kwargs)
File "/home/crito/.pythonbrew/venvs/Python-2.7.2/thirty-web/lib/python2.7/site-packages/django/views/generic/detail.py", line 100, in get
return self.render_to_response(context)
AttributeError: 'ActivationView' object has no attribute 'render_to_response'
In my eyes it shouldn't even call render_to_response. Any ideas?
If you want to leave out the confirmation page, just call your DeleteView directly with POST. This is most desirable as the deletion of an object should be protected by csrf.
You've inherited from BaseDeleteView, which as the documentation states, doesn't include the TemplateResponseMixin - ie all the bits that are to do with rendering a response.
Inherit from DeleteView instead.