Rest API token based authentication. Basic Auth - python

I have implemented token based authentication using rest_framework, every thing is working fine as of now but I have one question that if I am sending user-name and password in header using basic-auth then why am I required to send user-name and password with payload. When I sending request containing payload with user-name and password in body section with out user-name and password in header of request, I am getting following as response:
{
"detail": "CSRF Failed: CSRF token missing or incorrect."
}
while I am sending user-name and password in header with out sending user-name and password with payload, I am getting following response:
{
"detail": "Invalid username/password."
}
Why am I required to send user-name and password in header section while I am sending it with payload. I am not quite sure about the concept. Could anyone please explain and show me the right way to do it. I have used this reference.
Following is my code:
authuser.py
"""Authentication classes for Django Rest Framework.
Classes:
ExpiringTokenAuthentication: Authentication using extended authtoken model.
"""
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from rest_framework_expiring_authtoken.models import ExpiringToken
class ExpiringTokenAuthentication(TokenAuthentication):
"""
Extends default token auth to have time-based expiration.
Based on http://stackoverflow.com/questions/14567586/
"""
model = ExpiringToken
def authenticate_credentials(self, key):
"""Attempt token authentication using the provided key."""
if key == "2572e6dbe3b2e150764cd72712713b2975785197":
token = self.model.objects.get(key=key)
else:
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
#if token.expired():
# raise exceptions.AuthenticationFailed('Token has expired')
return (token.user_id, token)
view .py
class ObtainExpiringAuthToken(ObtainAuthToken):
"""View enabling username/password exchange for expiring token."""
model = ExpiringToken
def post(self, request):
try:
payload=json.loads(request.body)
except:
return Response({'success':False,'message':'Invalid Payload.'})
"""Respond to POSTed username/password with token."""
serializer = AuthTokenSerializer(data=request.data)
if serializer.is_valid():
token, _ = ExpiringToken.objects.get_or_create(
user=serializer.validated_data['user']
)
if token.expired():
# If the token is expired, generate a new one.
token.delete()
token = ExpiringToken.objects.create(
user=serializer.validated_data['user']
)
try:
user = User.objects.get(id=token.user_id)
except:
return Response({'success':False,'message':'User does not exists.'})
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()
models.py :
#receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
class ExpiringToken(Token):
"""Extend Token to add an expired method."""
class Meta(object):
proxy = True
def expired(self):
"""Return boolean indicating token expiration."""
now = timezone.now()
#if self.created < now - token_settings.EXPIRING_TOKEN_LIFESPAN:
# return True
return True
settings.py:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
)
}

The only possible way to get this error is by having SessionAuthentication enabled. It's enabled globally by default.
http://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme
Please double check you settings or set a proper one in you view class to override the global one.

Related

Django custom authentication class is not reading AllowAny

Here is the REST authentication class:
def get_authorization_header(request):
raw_token = request.COOKIES.get('auth_token', ) or None
auth = request.META.get('HTTP_AUTHORIZATION', )
if isinstance(auth, str):
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class JWTAuthentication(BaseAuthentication):
keyword = 'auth_token'
def authenticate(self, request):
raw_token = request.COOKIES.get('auth_token', ) or None
if raw_token is None:
return None
return self.authenticate_credentials(raw_token)
def authenticate_credentials(self, key):
try:
user_model = get_user_model()
payload = jwt.decode(key, settings.SECRET_KEY, algorithms="HS256")
user = user_model.objects.get(email=payload['email'])
except (jwt.DecodeError, user_model.DoesNotExist):
raise exceptions.ParseError('Invalid token')
except jwt.ExpiredSignatureError:
raise exceptions.ParseError('Token has expired')
if not user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
return (user, payload)
def authenticate_header(self, request):
return self.keyword
And here is the view:
class GoogleLogin(APIView):
permission_classes = [AllowAny]
def post(self, request):
data = request.data
response = Response()
token = data.get('tokenId', None)
if not token:
raise exceptions.AuthenticationFailed('No credentials provided.')
try:
token_info = id_token.verify_oauth2_token(token, requests.Request(), google_app_id)
email = token_info['email']
user = authenticate(email)
if not user:
serializer = RegisterSerializer(data={'email': token_info['email'], 'first_name': token_info['given_name'], 'last_name': token_info['family_name']})
serializer.is_valid(raise_exception=True)
serializer.save()
jwt_token = gen_token(email)
response.set_cookie(
key='auth_token',
value=jwt_token,
expires=datetime.datetime.utcnow() + datetime.timedelta(days=30),
secure=False,
httponly=True,
samesite='Lax'
)
return response
except ValueError:
return Response('Invalid TokenId.', status=status.HTTP_400_BAD_REQUEST)
I'm trying to implement Google social login where the frontend (ReactJS) sends tokenId to the backend (Django) to verify the token then returns a response with a JWT token stored in the cookies (cookie name is auth_token) as shown by this line response.set_cookie now when I try to login while I don't have the authentication token (I don't have auth_token in my cookies) everything works fine but when I try to login while I have an expired authentication token I get 'Token has expired' message even though I set the permission class to AllowAny
I think I implemented my authentication class wrong by I can't figure out where the issue is
AllowAny comes after authentication for checking registered or anonymous user has permission. If auth fail, request is blocked before permissions.
On your code, if there is no token, anonymous user can access login view. Auth accepts request.
There is no way for ExpiredToken to pass auth, because you raise exception.
Actually, your code does the right thing. If token is expired, front should delete invalid token, redirect to login page and make user push login button again without COOKIE.

Return appropriate error for unactivated users in Djoser

I am using Django 2.2.14 with the configuration below for Djoser 2.1.0 but when trying to get JWT token for an inactive user, it returns the same error as using a wrong password which makes it tricky to differentiate.
I get HTTP STATUS 401 with the detail below
{ "detail": "No active account found with the given credentials }
My configuration Djoser is shown below:
'LOGIN_FIELD': 'email',
'SEND_CONFIRMATION_EMAIL': True,
'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True,
'USER_CREATE_PASSWORD_RETYPE': True,
'TOKEN_MODEL': None,
'SEND_ACTIVATION_EMAIL': True,
"LOGOUT_ON_PASSWORD_CHANGE": False,
"PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND": True,
"USERNAME_RESET_SHOW_EMAIL_NOT_FOUND": True,
'PASSWORD_RESET_CONFIRM_URL': 'account/password/reset/confirm/{uid}/{token}',
'USERNAME_RESET_CONFIRM_URL': 'account/username/reset/ /{uid}/{token}',
'ACTIVATION_URL': 'account/activate/{uid}/{token}',
I am also using AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']
After some digging, it was noticed that Djoser uses simple-jwt module to generate its JWT tokens and hence, the error messages were coming from that module instead.
With help from some of the guys who contribute to simple-jwt, I was able to modify the code as shown below to make the endpoint check if a user is inactive and send appropriate error.
# custom_serializers.py
from django.contrib.auth.models import update_last_login
from rest_framework_simplejwt.serializers import TokenObtainSerializer
from rest_framework_simplejwt.exceptions import AuthenticationFailed
from rest_framework import status
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.tokens import RefreshToken
class InActiveUser(AuthenticationFailed):
status_code = status.HTTP_406_NOT_ACCEPTABLE
default_detail = "User is not active, please confirm your email"
default_code = 'user_is_inactive'
# noinspection PyAbstractClass
class CustomTokenObtainPairSerializer(TokenObtainSerializer):
#classmethod
def get_token(cls, user):
return RefreshToken.for_user(user)
def validate(self, attrs):
data = super().validate(attrs)
if not self.user.is_active:
raise InActiveUser()
refresh = self.get_token(self.user)
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
if api_settings.UPDATE_LAST_LOGIN:
update_last_login(None, self.user)
return data
# custom_authentication.py
def custom_user_authentication_rule(user):
"""
Override the default user authentication rule for Simple JWT Token to return true if there is a user and let
serializer check whether user is active or not to return an appropriate error.
Add 'USER_AUTHENTICATION_RULE': 'path_to_custom_user_authentication_rule' to simplejwt settings to override the default.
:param user: user to be authenticated
:return: True if user is not None
"""
return True if user is not None else False
# views.py
from .custom_serializer import CustomTokenObtainPairSerializer, InActiveUser
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken, TokenError
from rest_framework_simplejwt.views import TokenViewBase
class CustomTokenObtainPairView(TokenViewBase):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
Returns HTTP 406 when user is inactive and HTTP 401 when login credentials are invalid.
"""
serializer_class = CustomTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except AuthenticationFailed:
raise InActiveUser()
except TokenError:
raise InvalidToken()
return Response(serializer.validated_data, status=status.HTTP_200_OK)
# urls.py
path('api/token/', CustomTokenObtainPairView.as_view(),
name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),

Adding custom user authentication to django-rest-framework-simple-jwt

I want to add user login via One-time Password as well as the usual username/password method in django. In order to do so, either username/password or username/OTP are sent from client to sever and based on the provided pair of fields, I need to return access and refresh token if the user is authenticated. I am using django's simple-jwt. I know that I have to override TokenObtainPairView and TokenObtainSerializer. The problem is, I want to do the field validation part myself.
In my views, I override simple-jwt's default view.
#views.py
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
And I override the serializer like below:
#serializers.py
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
try:
request = self.context["request"]
except KeyError:
pass
try:
request_data = json.loads(request.body)
if("username" in request_data and "password" in request_data):
# default scenario in simple-jwt
pass
elif("username" in request_data and "otp" in request_data):
# validate username/otp manually and return access/token pair if successful
pass
else:
# some fields were missing
raise serializers.ValidationError({"username/otp or username/password" : "These fields are required"})
except:
pass
So, if client passes user credentials in one of the possible forms below, I will be able to authenticate it and return token pair.
{
"username" : "Winston",
"password" : "testpass"
}
or
{
"username" : "Winston",
"otp" : "testotp"
}
The problem is, when I send data in the second form, I get 400 BadRequest:password is required. How can I customize fields and their validation?
As Saiful Azad mentioned in comments, one possible method is to use separate serializers for each scenario.
#views.py
class MyTokenObtainPairView(TokenObtainPairView):
def get_serializer_class(self):
if ("otp" in self.request.data):
return MyTokenObtainPairSerializer
return TokenObtainPairSerializer
Then, you can implement your own serializer for otp verification. I used simple-jwt's implementation to implement my own serializer and use my custom authentication method.
In your urls.py
# Imports
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth.models import User
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
#api_view(['GET'])
#permission_classes([AllowAny])
def get_tokens_for_user(request):
# find the user base in params
user = User.objects.first()
refresh = RefreshToken.for_user(user)
return Response({
'refresh': str(refresh),
'access': str(refresh.access_token),
})
urlpatterns = [
path('login', get_tokens_for_user, name="login")
]

Why do we need to send JWT from client for every API after login, as DRF maintains isAuthenticated Flag after login in settings.py?

I have just started to explore django. I want to know whether JWT should be verified for every view manually in the server side or is there any better way of doing it.
Login View:
class LoginUser(APIView):
permission_classes = ()
def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
payload = jwt_payload_handler(user)
return Response({
'response_code':'success',
'response_msg':'Login successfull',
'username':user.username,
'token': jwt.encode(payload, SECRET_KEY)
},status.HTTP_200_OK)
else:
return Response(
{'response_code':'error',
'response_msg':'Invalid credentials'},status.HTTP_400_BAD_REQUEST
)
Menu View (to fetch menu)
def getMenu(request):
menu_list = serializers.serialize("json", Menu.objects.all())
return HttpResponse(menu_list)
settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}
My Login View returns JWT token in response. Login view works fine, but post login if I want to fetch Food Menu without sending JWT(got in response of login view), i am able to get the menu list, but ideally it should fail, saying Token is missing and should not return the response, but i am getting response.
I am curious to know, if DRF is only checking whether user is Authenticated or not and based on that if it returns response, then what is the point of JWT.
Your menu view looks like a normal django view. You have to use a DRF view
since you've only configured authentication and permission for DRF.

DRF responses me by 403 error when I try to request as a client [Client Credential grant]

In settings.py file I have written this settings:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
When I call any API with a token from a password grant application, then It's working fine, but when I try to call the same APIs with a token from a client credential grant application, it doesn't work and it responses with 403 error:
{ "detail": "You do not have permission to perform this action." }.
Is it because of default permission? I don't know what permission do I have to use instead!?
Finally solved! The problem was the permission I was using. In fact, the IsAuthenticated permission checks request.user which is None when you are using client credentials grant. Since there's no permission for supporting clien credentials grant in DRF, you must use your own DRF custom permission. This is what I needed and used:
from rest_framework.permissions import BasePermission
class IsAuthenticatedOrClientCredentialPermission(BasePermission):
def has_permission(self, request, view):
if request.auth is None:
return False
grant_type = request.auth.application.get_authorization_grant_type_display()
if request.user is None:
if grant_type == 'Client credentials':
request.user = request.auth.application.user # <-- this is because I needed to get the user either the grant is 'password' or 'client credentials'
return True
else:
return False
else:
return True
But you may want to have a permission just for checking if the grant type is client credentials and give the permission, if so, this is what you need:
from rest_framework.permissions import BasePermission
class ClientCredentialPermission(BasePermission):
def has_permission(self, request, view):
if request.auth is None:
return False
grant_type = request.auth.application.get_authorization_grant_type_display()
if request.user is None and grant_type == 'Client credentials':
return True
else:
return False
Note: if you want to use the second custom permission, be aware that the request.user is None and you can get the owner of the client (that is sending request to you) via request.auth.application.user.
Using (custom) permissions:
You can use your custom permission by adding them to proper views. (Just like using any DRF permissions under rest_framework.permissions)
class-based views:
class ExampleView(APIView):
permission_classes = [ClientCredentialPermission] # <-- Add your permissions to this list
def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
function-based views:
#api_view(['GET'])
#permission_classes([ClientCredentialPermission]) # <-- Add your permissions to this list
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
I had the same problem. The issue in my case was the #authentication_classes that was originally enabled for when I was using credentials directly (not token). I removed them (see them below commented out). Idea came after reading the first answer here.
This works for me as I only want token base access, and so I don't need the other authentication classes. This is how my view decoration looks like:
#api_view(['GET'])
##authentication_classes([SessionAuthentication, BasicAuthentication])
#permission_classes([IsAuthenticated])
def apilink(request, format=None):
.....
you need to enable tokenAuthentication and run migration to apply changes in auth table DB
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', # <-- And here
],
}
INSTALLED_APPS = [
...
'rest_framework.authtoken'
]
Here is the perfect blog for your usecase.
https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
https://simpleisbetterthancomplex.com/tutorial/2018/11/22/how-to-implement-token-authentication-using-django-rest-framework.html#implementing-the-token-authentication

Categories