Django Graphql middleware decode token - python

In my project I used Django and GraphQl for building API. User will be authenticated by API Gateway in AWS, and send a JWT token, with uuid username body, included in the request headers to the backend.
I need to decode that token and get an username value, that will be next used in the resolvers.
I have planned to use something similar as G object in Flask or something similar using Rack Djangos middleware but I'm struggling how to do it in Django.
Do you have any idea or hint?

Here's the result which I implemented:
The middleware checks the jwt token before resolver call, based on decoded username it create a User instance, that is assigned in info.context.user parameter.
The info.context will be visible in resolver.
So basically in resolver you can check a User instance as:
user = info.context.user
if isinstance(user, User):
# do something
middleware.py
class AuthorizationGraphQLMiddleware:
"""Middleware add User object for each GraphQL resolver info.context"""
def resolve(self, next, root, info, **kwargs):
username = None
auth_header = info.context.META.get('HTTP_AUTHORIZATION')
if auth_header:
username = decode_token(auth_header)['username']
if username is not None:
info.context.user = User(username)
else:
info.context.user = AnonymousUser()
return next(root, info, **kwargs)
entities.py
#dataclass
class User:
name: str
utils.py
class TokenValidationError(GraphQLError):
pass
def decode_token(token):
try:
return jwt.decode(token.replace('Bearer ', ''), verify=False)
except (jwt.DecodeError, AttributeError):
raise TokenValidationError('Invalid token.')
settings.py
GRAPHENE = {
'MIDDLEWARE': ('api.graphql.middleware.AuthorizationGraphQLMiddleware',)
}

Related

Flask RESTful API - authorization for specific user or admin user

I am developing a Flask API using Flask Restful. I wonder if there is a clean way for authorizing users that would not force me to code duplication. I use Flask-JWT-Extended for authentication in my API.
I have got some endpoints that I want to be accessible only by user with admin role OR the user, that is related to a given resource.
So, let's say I'd like to enable user to obtain information about their account and I'd like to prevent other users from accessing this information. For now, I am solving it this way:
from flask import request, Response
from flask_restful import Resource, reqparse
from flask_jwt_extended import (create_access_token, create_refresh_token, jwt_required, get_jwt_identity)
from app.mod_auth.models import User
[...]
class UserApi(Resource):
#jwt_required()
def get(self, name):
current_user = User.find_by_login(get_jwt_identity())
if current_user.login==name or current_user.role==1:
user = User.query.filter_by(login=name).first_or_404(description="User not found")
return user.json()
else:
return {'message': 'You are not authorized to access this data.'}, 403
[...]
So first I check, if there's correct and valid JWT token in the request, and then, basing on the token I check if the user related with the token is the same, as the user, whose data is being returned. Other way for accessing data is user with role 1, which I treat as an administrative role.
This is the part of my User's model:
[...]
class User(Base):
login = db.Column(db.String(128), nullable=False)
email = db.Column(db.String(128), unique=True, nullable=False)
password = db.Column(db.String(192), nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False)
role = db.Column(db.SmallInteger, default=0, nullable=False)
[...]
Of course, soon I'll have a few endpoints with data specific for user.
I have found an example of custom operator in Flask-JWT-Extended, that provides an authorization for admin users: flask-jwt-extended admin authz - but on the other hand, it does not support user-specific authorization. I have no idea, how to improve that snippet in order to verify, if the user requesting for an resource is a specific user with rights to the resource.
How can I define a custom operator, that will provide correct access to the user-specific data?
Maybe I should include some kind of owner data in each DB model, that should support authorization, and verify that in the requests, as in the example above?
Thanks in advance
You can create a decorator that checks the identity of the user:
def validate_user(role_authorized:list() = [1]):
def decorator(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
name = request.path.rsplit('/', 1)[-1]
current_user = User.find_by_login(get_jwt_identity())
if (current_user.login == name) or (current_user.role in role_authorized):
kwargs["logged_user"] = current_user # If you need to use the user object in the future you can use this by passing it through the kwargs params
return fn(*args, **kwargs)
else:
return {'message': 'You are not authorized to access this data.'}, 403
return wrapper
return decorator
class Test(Resource):
#jwt_required()
#list of the roles authorized for this endpoint = [1]
#validate_user([1])
def post(self, name, **kwargs):
#logged_user = kwargs["logged_user"] # Logged in User object
user = User.query.filter_by(login=name).first_or_404(description="User not found")
return user.json()

Django JWT - Allow request with no credential if key in body is valid

I have an endpoint that needs an Access Token for authentication.
Also in this endpoint, there's a serializer UUID field that is not required.
What I would like to do is:
In case the UUID field is passed in the body, check if this UUID is valid and registered in the Database.
If it's not valid it'll raise a DRF ValidationError.
If no UUID field is passed then the request needs to be authenticated with an Access Token.
Is there a way to have this kind of seletive auth verification?
Well, after some time I figured out the solution.
I opened the permission to access the view to any user, and inside the post method, I verified if the user is anonymous in order to check the request body keys. In case a valid JWT access token is passed, a user would be found and this would return False. If any of these conditions return True a simple credential error response is given.
permission_classes = [
AllowAny,
]
def post(self, request, *args, **kwargs):
if request.user.is_anonymous:
request_token = request.POST.get('token')
token = Token.objects.filter(token=request_token)
if token.exists() and token.values()[0]['status'] == 'valid':
pass
else:
return Response(
data={"Invalid Credentials": "Pass a valid credential."},
status=status.HTTP_200_OK,

How can I authenticate a user with a query parameter on any url?

Let's say the user lands on https://example.com/any/page?token=hhdo28h3do782.
What's the recommended way to authenticate and login a user with the query string?
I was thinking about creating some sort of catch-all view (I'd also like to know how to do this :D) that calls authenticate(). Then I would have in place a custom backend that would authenticate the user.
Is this the ideal way to achieve what I want?
Cheers!
To do this, you need to create a custom authentication backend that validates api keys.
In this example, the request is checked for a valid token automatically. You don't need to modify and of your views at all. This is because it includes custom middleware that authenticates the user.
For brevity, I'm assuming that the valid user tokens are stored in a model that is foreign keyed to the django auth.User model.
# my_project/authentication_backends.py
from django.contrib import auth
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth.middleware import AuthenticationMiddleware
TOKEN_QUERY_PARAM = "token"
class TokenMiddleware(AuthenticationMiddleware):
def process_request(self, request):
try:
token = request.GET[TOKEN_QUERY_PARAM]
except KeyError:
# A token isn't included in the query params
return
if request.user.is_authenticated:
# Here you can check that the authenticated user has the same `token` value
# as the one in the request. Otherwise, logout the already authenticated
# user.
if request.user.token.key == token:
return
else:
auth.logout(request)
user = auth.authenticate(request, token=token)
if user:
# The token is valid. Save the user to the request and session.
request.user = user
auth.login(request, user)
class TokenBackend(ModelBackend):
def authenticate(self, request, token=None):
if not token:
return None
try:
return User.objects.get(token__key=token)
except User.DoesNotExist:
# A user with that token does not exist
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Now, you can add the paths to AUTHENTICATION_BACKENDS and MIDDLEWARE in your settings.py in addition to any existing backends or middleware you may already have. If you're using the defaults, it would look like this:
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
# This is the dotted path to your backend class. For this example,
# I'm pretending that the class is in the file:
# my_project/authentication_backends.py
"my_project.authentication_backends.TokenMiddleware",
# ...
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"my_project.authentication_backends.TokenBackend",
]
I assume you are using the Django REST Framework and also enabled the TokenAuthentication mechanism in your project. If so, go ahead with this,
from rest_framework.authentication import TokenAuthentication
class QueryParamAuthentication(TokenAuthentication):
query_param_name = 'token'
def authenticate(self, request):
token = request.query_params.get(self.query_param_name)
if token:
return self.authenticate_credentials(token)
return None
and then, change DRF DEFAULT_AUTHENTICATION_CLASSES as
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'dotted.path.to.QueryParamAuthentication'
),
# rest of your DRF settings...
}
Update
to do this without DRF, you have to write custom model backend (which is a bit lengthy topic)
Refer: Writing an authentication backend
So, start with a way of managing your tokens. Here's a basic model:
class Token(models.Model):
code = models.CharField(max_length=255)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
expires = models.DateTimeField()
A custom authentication backend can be produced to check the validity of the tokens:
class TokenAuthenticationBackend(ModelBackend):
def authenticate(self, request, token=None):
try:
token = Token.objects.get(code=token, expires__gte=now())
except Token.DoesNotExist:
return None
else:
return token.user
If you're using class-based views, you could write a mixin that checks for the presence of the token then does your authentication logic:
class UrlTokenAuthenticationMixin:
def dispatch(self, request, *args, **kwargs):
if 'token' in request.GET:
user = authenticate(request, request.GET['token'])
if user:
login(request, user)
return super(UrlTokenAuthenticationMixin, self).dispatch(request, *args, **kwargs)
To use this on a given view, just declare your views as follows:
class MyView(UrlTokenAuthenticationMixin, TemplateView):
# view code here
For example.
An alternative way to implement this as a blanket catch-all would be to use middleware rather than a mixin:
class TokenAuthMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if 'token' in request.GET:
user = authenticate(request, request.GET['token'])
if user:
login(request, user)
return self.get_response(request)

Basic auth protected views in DRF

I have some API endpoints that i need to protect using HTTP Basic Authentication in Django Rest Framework. There is BasicAuthentication in DRF, but that actually authenticates against a user in Django, which is not what I'm looking for.
I found a solution using a custom permission, but ti means monkey patching the views to set the correct authenticate header.
Is there a better way?
class BasicAuthPermission(permissions.BasePermission):
def has_permission(self, request, view):
credentials = view.credentials # Will raise AttributeError on missing credentials
realm = getattr(view, 'realm', 'Protected')
auth = request.headers.get('Authorization')
with suppress(ValueError, AttributeError):
auth = b64decode(auth.split()[-1]).decode()
if auth != credentials:
# Monkey patch style
view.get_authenticate_header = lambda r: f'Basic realm="{realm}"'
raise exceptions.AuthenticationFailed('Bad credentials.')
return True
Im my view:
class ProtectedApiView(generics.GenericAPIView):
permission_classes = [BasicAuthPermission]
credentials = 'user:password'
# ...
Following Arakkal's suggestion in comment, I did this with an Authentication class instead. It does feel less hacky, but I can not set credentials on the View, like I did originally.
I realize "anonymous authentication" is a weird name, but that's because Django doesn't know anything about the user. So for all practical purposes anonymous.
from base64 import b64decode
import binascii
from rest_framework import generics, exceptions, authentication
class AnonymousBasicAuthentication(authentication.BaseAuthentication):
"""
HTTP Basic authentication against preset credentials.
"""
www_authenticate_realm = 'api'
credentials: str = None
def authenticate(self, request):
try:
auth, encoded = authentication.get_authorization_header(request).split(maxsplit=1)
except ValueError:
raise exceptions.AuthenticationFailed('Invalid basic header.')
if not auth or auth.lower() != b'basic':
raise exceptions.AuthenticationFailed('Authentication needed')
try:
credentials = b64decode(encoded).decode(authentication.HTTP_HEADER_ENCODING)
except (TypeError, UnicodeDecodeError, binascii.Error):
raise exceptions.AuthenticationFailed('Invalid basic header. Credentials not correctly base64 encoded.')
if self.credentials != credentials:
raise exceptions.AuthenticationFailed('Invalid username/password.')
def authenticate_header(self, request):
return 'Basic realm="{}"'.format(self.www_authenticate_realm)
class MyAuthentication(AnonymousBasicAuthentication):
credentials = 'user:password'
class MyProtectedView(generics.GenericAPIView):
authentication_classes = [MyAuthentication]
# ...

How can I use the jwt token for authentication that i get from my login view

I need to create JWT token authentication, but I don't know how, could you explain me how to do it better or put some examples?
my view:
class UserLogin(generics.CreateAPIView):
"""
POST auth/login/
"""
# This permission class will overide the global permission
# class setting
permission_classes = (permissions.AllowAny,)
queryset = User.objects.all()
serializer_class = TokenSerializer
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
user = auth.authenticate(request, username=username, password=password)
if user is not None:
auth.login(request, user)
return Response({
"token": jwt_encode_handler(jwt_payload_handler(user)),
'username': username,
}, status=200)
return Response(status=status.HTTP_401_UNAUTHORIZED)
You are creating the token in that view. After that, you need two other mechanism in place:
Your client should send this token the the API with each request, in the Authorization header, like:
Authorization: Bearer your_token
On the api side, you need to use an authentication class, that looks for Authorization header, takes the token and decodes it, and finds the user instance associated with the token, if the token is valid.
If you are using a library for drf jwt authentication, it should have an authentication class that you can use. If you are implementing it manually, you need to write an authentication class that subclasses DRF's BaseAuthentication class yourself. It could basically look like this:
class JwtAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth_header = request.META.get('HTTP_AUTHORIZATION')
if auth_header:
key, token = auth_header.split(' ')
if key == 'Bearer':
# Decode the token here. If it is valid, get the user instance associated with it and return it
...
return user, None
# If token exists but it is invalid, raise AuthenticationFailed exception
# If token does not exist, return None so that another authentication class can handle authentication
You need to tell DRF to use this authentication class. Add this to your settings file for that:
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': [
'path.to.JwtAuthentication',
...
]
}

Categories

Resources