I am trying to restrict list view using Django Rest Framework.
If I am sending request with wrong token:
Authorization: Token f1cfedb0895105ee088e8aab5bf0ae7ca9752e79
It returns me HTTP 401 error which is correct
But if i just remove Authorization from HTTP Headers it will query without authentication and return HTTP 200
Am i doing something wrong here:
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
model = Order
#permission_classes((IsAuthenticated,))
#authentication_classes((TokenAuthentication,))
def list(self, request, *args, **kwargs):
serializer = OrderSerializer(Order.objects.opened(), many=True)
return Response(serializer.data)
Also create method should not be restricted to authenticated users.
I would suggest standardizing your Viewset:
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
model = Order
def get_queryset(self):
"""QuerySet for this entire ModelViewSet will only return
orders which are opened.
"""
return Order.objects.opened()
#permission_classes((IsAuthenticated,))
#authentication_classes((TokenAuthentication,))
def list(self, request, *args, **kwargs):
return super(OrderViewSet, self).list(request, *args, **kwargs)
Upon further investigation, I took a look at the source code for TokenAuthentication, and it appears that if you don't send in an authentication token at all, the authenticate() method returns None if the get_authorization_header() method returns nothing. Thus, if you entirely remove the HTTP_AUTHORIZATION from the header, this is the expected behavior.
I believe the intention here is to not raise an exception so that authentication can move on to the next possible authentication class. If this is not what you want to do, you can override the authenticate() method in your own class inherited from TokenAuthentication. See the code below.
def get_authorization_header(request):
"""
Return request's 'Authorization:' header, as a bytestring.
Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, type('')):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class TokenAuthentication(BaseAuthentication):
"""
Simple token based authentication.
Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string "Token ". For example:
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
"""
model = Token
"""
A custom token model may be used, but must have the following properties.
* key -- The string identifying the token
* user -- The user to which the token belongs
"""
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'token':
return None
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = 'Invalid token header. Token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
def authenticate_credentials(self, key):
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')
return (token.user, token)
def authenticate_header(self, request):
return 'Token'
Finally, to make your token fail with a 401 instead of a 200, you can do:
class YourCustomTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'token':
msg = 'Invalid token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
if len(auth) == 1:
msg = 'Invalid token header. No credentials provided.'
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = 'Invalid token header. Token string should not contain spaces.'
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
Related
I want to set the bearer token in the authorization header. why? Because
I am using rest_framework_simplejwt in my Django project. I want to implement authorization
using JWT. For that, I create an access token and save it in cookies on user login.
Now I want to check if the access token which is stored in the cookie is invalid or expired? So, the user can see the data fetched from DB.
Let me tell you some more detail then I will be able to tell you my problem.
when I change the token in the cookie manually and refreshed it
it just shows that I am login.
Is there any best way to send this Token to the frontend by including in header or if we can update the previous Token by new Token in Login View.
I am not getting that how to work with Django REST Framework default authtoken. Please guide me what is the standard process of using Token Based Authentication
view.py
ACCESS_TOKEN_GLOBAL=None
class Register(APIView):
RegisterSerializer_Class=RegisterSerializer
def get(self,request):
return render(request, 'register.html')
def post(self,request,format=None):
serializer=self.RegisterSerializer_Class(data=request.data)
if serializer.is_valid():
serializer.save()
msg={
'msg':"Registered Successfully"
}
return render(request, 'login.html',msg)
else:
return Response({"Message":serializer.errors,"status":status.HTTP_400_BAD_REQUEST})
class Login(APIView):
def get(self,request):
if 'logged_in' in request.COOKIES and 'Access_Token' in request.COOKIES:
context = {
'Access_Token': request.COOKIES['Access_Token'],
'logged_in': request.COOKIES.get('logged_in'),
}
return render(request, 'abc.html', context)
else:
return render(request, 'login.html')
def post(self,request,format=None):
email = request.POST.get('email')
password = request.POST.get('password')
print(email,password)
user = User.objects.filter(email=email).first()
if user is None:
raise AuthenticationFailed('User not found!')
if not user.check_password(password):
raise AuthenticationFailed('Incorrect password!')
refresh = RefreshToken.for_user(user)
# request.headers['Authorization']=str(refresh.access_token)
# request.
global ACCESS_TOKEN_GLOBAL
ACCESS_TOKEN_GLOBAL=str(refresh.access_token)
response=render(request,'students.html')
response.set_cookie('Access_Token',str(refresh.access_token))
response.set_cookie('logged_in', True)
return response
class StudentData(APIView):
StudentSerializer_Class=StudentSerializer
permission_classes=[IsAuthenticated]
def get(self,request,format=None):
token = request.COOKIES.get('jwt')
if token!=ACCESS_TOKEN_GLOBAL:
raise AuthenticationFailed('Unauthenticated!')
DataObj=Student.objects.all()
serializer=self.StudentSerializer_Class(DataObj,many=True)
serializerData=serializer.data
return Response({"status":status.HTTP_200_OK,"User":serializerData})
def post(self,request,format=None):
serializer=self.StudentSerializer_Class(data=request.data)
if serializer.is_valid():
serializer.save()
serializerData=serializer.data
return Response({"status":status.HTTP_200_OK,"User":serializerData})
else:
return Response({"Message":serializer.errors,"status":status.HTTP_400_BAD_REQUEST})
class Logout(APIView):
def post(self,request):
try:
response = HttpResponseRedirect(reverse('login'))
# deleting cookies
response.delete_cookie('Access_Token')
response.delete_cookie('logged_in')
return response
except:
return Response({"status":status.HTTP_400_BAD_REQUEST})
here is the image I get when I go on the student route to see the data.How I can fix it?
I have the token but how I will tell the server via that access_token and show the data using HTML page rather than just pass it to postman's bearer field
For postman:
Go under the tap 'headers'.
Create a new KEY: Authorization with VALUE: Token <>
That's it, your token authorization is in the header.
You can do that in every request created in postman. I'm still looking for a way to change the header in the class-based view to add the token authorization as it is not working in the APIView.
I have created a DRF api authenticated with jwt,the token is stored in a cookie.I can successfully access all the viewsets using the token with postman.It only becomes a problem when l want to pass the token to angular frontend for the same operations.I am using django rest framework backend and Angular 9 frontend.Also note that l am storing the token in a cookie.
My views.py
class LoginView(APIView):
def post(self,request):
#getting the inputs from frontend/postman
email =request.data['email']
password =request.data['password']
user=User.objects.filter(email=email).first()
#Authentication
if user is None:
raise AuthenticationFailed('User not found!')
if user.password!=password :
raise AuthenticationFailed("incorrect password")
payload = {
'id':user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=10),
'iat': datetime.datetime.utcnow()
}
token = jwt.encode(payload, 'secret', algorithm='HS256')
response = Response()
#storing the token in a cookie
response.set_cookie(key='jwt',value=token ,httponly=True)
response.data = {
'jwt':token
}
return response
class UserView(APIView):
def get(self,request):
token=request.COOKIES.get('jwt')
if not token:
raise AuthenticationFailed("unauthorised")
try:
payload =jwt.decode(token, 'secret', algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("session expired")
user=User.objects.get(id=payload['id'])
serializer=UserSerializer(user)
return Response(serializer.data)
class Update(APIView):
def get_object(self,request):
try:
token=request.COOKIES.get('jwt')
if not token:
raise AuthenticationFailed("unauthorised")
try:
payload =jwt.decode(token, 'secret', algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("session expired")
user=User.objects.get(id=payload['id'])
return user
except User.DoesNotExist:
return Response("wakadhakwa",status=status.HTTP_204_NO_CONTENT)
def get(self,request):
obj=self.get_object(request)
serializer=UserSerializer(obj)
return Response(serializer.data)
def put(self,request):
obj=self.get_object(request)
serializer=UserSerializer(obj,data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response("corrupted data",status=status.HTTP_204_NO_CONTENT)
def delete(self,request):
all=self.get_object(request)
all.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Did you check that the cookie gets properly saved in browser when receiving response from login?
Are you calling the UserView endpoints from your Angular app with an AJAX call or are you reloading the page? If it is a call from the app make sure that the request sends cookies. It depends on how exactly you request the data, e.g. if you're using fetch, then make sure you have the option credentials: 'include' set. If you're requesting the data in some other way try to find in the documentation which option is used to enable sending credentials (cookies).
I want to send the SimpleJWT access and refresh tokens through HttpOnly cookie. I have customized the claim. I have defined a post() method in the MyObtainTokenPairView(TokenObtainPairView) in which I am setting the cookie. This is my code:
from .models import CustomUser
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
serializer = self.serializer_class()
response = Response()
tokens = serializer.get_token(CustomUser)
access = tokens.access
response.set_cookie('token', access, httponly=True)
return response
It's returning this error:
AttributeError: 'RefreshToken' object has no attribute 'access'
The serializer:
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
#classmethod
def get_token(cls, user):
print(type(user))
token = super().get_token(user)
token['email'] = user.email
return token
But it's just not working. I think I should not define a post() method here like this. I think if I can only return the value of the get_token() function in the serializer, I could set it as HttpOnly cookie. But, I don't know how to do that.
How do I set the access and refresh tokens in the HttpOnly cookie?
EDIT:
I made these changes following anowlinorbit's answer:
I changed my serializer to this:
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
attrs = super().validate(attrs)
token = self.get_token(self.user)
token["email"] = self.user.email
return token
Since this token contains the refresh token by default therefore, I decided that returning only this token would provide both access and refresh token.
If I add anything like token["access"] = str(token.access_token) it would just add the access token string inside the refresh token string, which it already contains.
But again in the view, I could not find how to get the refresh token. I could not get it using serializer.validated_data.get('refresh', None) since now I am returning the token from serializer which contains everything.
I changed my view to this:
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
response.set_cookie('token', token, httponly=True)
return response
Now it's saying:
NameError: name 'token' is not defined
What's wrong here? In the view I want to get the token returned from serializer, then get the acces token using token.access_token and set both refresh and access as cookies.
I would leave .get_token() alone and instead focus on .validate(). In your MyTokenObtainPairSerializer I would remove your changes to .get_token() and add the following
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data["refresh"] = str(refresh) # comment out if you don't want this
data["access"] = str(refresh.access_token)
data["email"] = self.user.email
""" Add extra responses here should you wish
data["userid"] = self.user.id
data["my_favourite_bird"] = "Jack Snipe"
"""
return data
It is by using the .validate() method with which you can choose which data you wish to return from the serializer object's validated_data attribute. N.B. I have also included the refresh token in the data which the serializer returns. Having both a refresh and access token is important. If a user doesn't have the refresh token they will have to login again when the access token expires. The refresh token allows them to get a new access token without having to login again.
If for whatever reason you don't want the refresh token, remove it from your validate() serializer method and adjust the view accordingly.
In this post method, we validate the serializer and access its validated data.
def post(self, request, *args, **kwargs):
# you need to instantiate the serializer with the request data
serializer = self.serializer(data=request.data)
# you must call .is_valid() before accessing validated_data
serializer.is_valid(raise_exception=True)
# get access and refresh tokens to do what you like with
access = serializer.validated_data.get("access", None)
refresh = serializer.validated_data.get("refresh", None)
email = serializer.validated_data.get("email", None)
# build your response and set cookie
if access is not None:
response = Response({"access": access, "refresh": refresh, "email": email}, status=200)
response.set_cookie('token', access, httponly=True)
response.set_cookie('refresh', refresh, httponly=True)
response.set_cookie('email', email, httponly=True)
return response
return Response({"Error": "Something went wrong", status=400)
If you didn't want the refresh token, you would remove the line beginning refresh = and remove the line where you add the refresh cookie.
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
token = response.data["access"] # NEW LINE
response.set_cookie('token', token, httponly=True)
return response
You can solve it by setting the token as the "access" from the response data.
Btw, did you find any better solution for this?
For the following piece of code, I would like to return a boolean corresponding to whether the user was authenticated or not.
class UserAuthenticatedView(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (AllowAny,)
def get(self, request, format=None):
is_authenticated = request.user.is_authenticated()
resp = {'is_authenticated': is_authenticated}
return Response(resp, content_type="application/json", status=status.HTTP_200_OK)
However, for invalid token, the control is not not even going inside get method due to which I'm not able to customize the response. In such a case I'm getting the response: {'detail': 'invalid token'},
Any idea on how to customize the response for invalid token ?
You can create a CustomTokenAuthentication class and override the authenticate_credentials() method to return the custom response in case of invalid token.
class CustomTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
try:
token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
# modify the original exception response
raise exceptions.AuthenticationFailed('Custom error message')
if not token.user.is_active:
# can also modify this exception message
raise exceptions.AuthenticationFailed('User inactive or deleted')
return (token.user, token)
After doing this, define this custom token authentication class in your DRF settings or on a per-view/viewset basis.
Another option is to create a custom exception handler. In that, you can check if the exception raised was of type AuthenticationFailed and the exception message is 'invalid token'. There you can modify the exception message (also check this official DRF example).
This worked for me:
Custom Authentication class:
class MyAuthentication(authentication.TokenAuthentication):
def authenticate_credentials(self, key):
try:
token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
return (None, '')
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
view class:
class UserAuthenticatedView(APIView):
authentication_classes = (MyAuthentication,)
permission_classes = (AllowAny,)
def get(self, request, format=None):
is_authenticated = False
if request.user and request.user.is_authenticated():
is_authenticated = True
resp = {'is_authenticated': is_authenticated}
return Response(resp, content_type="application/json", status=status.HTTP_200_OK)
everyone. I am trying to write tests for RESTful API implemented using django-tastypie with http basic auth. So, I have the following code:
def http_auth(username, password):
credentials = base64.encodestring('%s:%s' % (username, password)).strip()
auth_string = 'Basic %s' % credentials
return auth_string
class FileApiTest(TestCase):
fixtures = ['test/fixtures/test_users.json']
def setUp(self):
self.extra = {
'HTTP_AUTHORIZATION': http_auth('testuser', 'qwerty')
}
def test_folder_resource(self):
response = self.client.get('/api/1.0/folder/', **self.extra)
self.assertEqual(response.status_code, 200)
def test_folder_resource_post(self):
response = self.client.post('/api/1.0/folder/', **self.extra)
self.assertNotEqual(response.status_code, 401)
GET request is done well, returning status code 200. But POST request always returns 401. I am sure I am doing something wrong. Any advice?
Check out this question. I've used that code for tests using both GET and POST and it worked. The only difference I can see is that you have used base64.encodestring instead of base64.b64encode.
Otherwise, if that doesn't work, how are you performing the HTTP Authentication? I wrote and use this function decorator:
import base64
from django.http import HttpResponse
from django.contrib.auth import authenticate, login
def http_auth(view, request, realm="", must_be='', *args, **kwargs):
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2:
if auth[0].lower() == "basic":
uname, passwd = base64.b64decode(auth[1]).split(':')
if must_be in ('', uname):
user = authenticate(username=uname, password=passwd)
if user is not None and user.is_active:
login(request, user)
request.user = user
return view(request, *args, **kwargs)
# They mustn't be logged in
response = HttpResponse('Failed')
response.status_code = 401
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
return response
def http_auth_required(realm="", must_be=''):
""" Decorator that requires HTTP Basic authentication, eg API views. """
def view_decorator(func):
def wrapper(request, *args, **kwargs):
return http_auth(func, request, realm, must_be, *args, **kwargs)
return wrapper
return view_decorator
I've found a reason of my problem. DjangoAuthorization checks permissions with django premissions framework, since I don't use it in my project — all post/put/delete requests from non superuser are unauthorized. My bad.
Anyway, thanks a lot to you, guys, for responses.
On Python 3
#staticmethod
def http_auth(username, password):
"""
Encode Basic Auth username:password.
:param username:
:param password:
:return String:
"""
data = f"{username}:{password}"
credentials = base64.b64encode(data.encode("utf-8")).strip()
auth_string = f'Basic {credentials.decode("utf-8")}'
return auth_string
def post_json(self, url_name: AnyStr, url_kwargs: Dict, data: Dict):
"""
Offers a shortcut alternative to doing this manually each time
"""
header = {'HTTP_AUTHORIZATION': self.http_auth('username', 'password')}
return self.post(
reverse(url_name, kwargs=url_kwargs),
json.dumps(data),
content_type="application/json",
**header
)