DRF simplejwt refresh access_token stored in HTTPonly cookies - django

I'm doing a project using React and django, I have used a DRF SimpleJWT for authentication. I have stored a access and refresh token in HTTPOnly cookies all are working fine but I didn't find the way to refresh the token. I can't make it through by reading a documentation. If somebody has done it before please share the code

Hope I'm not late.
An easy way, you can use Dj-Rest-Auth to handle everything.
Otherwise,
If you want to use function views you can use in your views.py and add its URL to urls.py:
#api_view(['POST'])
#permission_classes([AllowAny])
#csrf_protect
def refresh_token_view(request):
User = get_user_model()
refresh_token = request.COOKIES.get('refreshtoken')
if refresh_token is None:
raise exceptions.AuthenticationFailed(
'Authentication credentials were not provided.')
try:
payload = jwt.decode(
refresh_token, settings.REFRESH_TOKEN_SECRET, algorithms['HS256'])
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed(
'expired refresh token, please login again.')
user = User.objects.filter(id=payload.get('user_id')).first()
if user is None:
raise exceptions.AuthenticationFailed('User not found')
if not user.is_active:
raise exceptions.AuthenticationFailed('user is inactive')
access_token = generate_access_token(user)
return Response({'access_token': access_token})
If you want to use class views add this to your views.py:
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken
class CookieTokenRefreshSerializer(TokenRefreshSerializer):
refresh = None
def validate(self, attrs):
attrs['refresh'] =
self.context['request'].COOKIES.get('refresh_token')
if attrs['refresh']:
return super().validate(attrs)
else:
raise InvalidToken('No valid token found in cookie\'refresh_token\'')
class CookieTokenObtainPairView(TokenObtainPairView):
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get('refresh'):
cookie_max_age = 3600 * 24 * 14 # 14 days
response.set_cookie('refresh_token',response.data['refresh'],max_age=cookie_max_age, httponly=True )
del response.data['refresh']
return super().finalize_response(request, response, *args, **kwargs)
class CookieTokenRefreshView(TokenRefreshView):
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get('refresh'):
cookie_max_age = 3600 * 24 * 14 # 14 days
response.set_cookie('refresh_token',response.data['refresh'], max_age=cookie_max_age, httponly=True )
del response.data['refresh']
return super().finalize_response(request, response, *args, **kwargs)
serializer_class = CookieTokenRefreshSerializer
Add the below in url.py to use the above views to get and refresh:
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
urlpatterns = [
path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
]
source

Related

How do I authenticate/protect non api_views with DRF + JWT eg. rendering a private page

I understand how to protect this for example, get the token, store it as cookie, then get the access token via the cookie and send it via Ajax to /api/hello and I get my JSON
#api_view()
#permission_classes([IsAuthenticated])
def hello(request):
return Response({'message': 'hello this is protected'})
But how would I protect this page where I want only people who are authenticated to see the page, eg. if they click this URL /secretmessage
def secret_message(request):
return render(request, 'secret_message.html')
Am I supposed to use a combination of JWT auth for the API stuff and Session Based auth and not just rely on JWT for all auth?
from django.contrib.auth.decorators import login_required
#login_required
def secret_message(request):
return render(request, 'secret_message.html')
Use #login_required decorator, to allows only users who are logged in to access the view.
If you want in this case you could create your own decorator which could answer if the user logged in with jwt or not.
This could be an example:
decorators.py
from functools import wraps
from django.http import JsonResponse
import jwt
def api_login_required(function):
#wraps(function)
def wrap(request, *args, **kwargs):
token = request.request.COOKIES.get('jwt')
if token == None:
return JsonResponse({'message':"Error: Unauthenticated..."})
try:
payload = jwt.decode(token, 'secret', algorithms=['HS256'])
except Exception: #jwt.ExpiredSignatureError:
return JsonResponse({'message':"Error: Unauthenticated..."})
if payload:
return function(request, *args, **kwargs)
else:
return JsonResponse({'message':"Error: Unauthenticated..."})
return wrap
Then you would only have to make the call with #api_login_required
from django.contrib.auth.decorators import login_required
#api_login_required
def secret_message(request):
return render(request, 'secret_message.html')

How to test a view that requires a user to be authenticated in Django?

I have the following view that requires a user to be authenticated to access it, otherwise redirects to the login page:
In my urls.py file:
from . import views
urlpatterns = [
path("settings", login_required(views.Settings.as_view()), name="settings"),
]
In my views.py file:
class Settings(View):
def get(self, request):
return render(request, "project/settings.html")
How can I test this view?
Here is what I tried:
class TestViews(TestCase):
def setUp(self):
self.user = User.objects.create(
username="testUser",
password="testPassword",
)
def test_settings_authenticated(self):
client = Client()
client.login(username="testUser", password="testPassword")
response = client.get("/settings")
self.assertEquals(response.status_code, 200)
self.assertTemplateUsed(response, "project/settings.html")
But this returns a 302 status, implying the user was not authenticated.
Instead of User.objects.create(...) use User.objects.create_user(...) since the create method will not automatically hash the password.
This solved my problem.

django-axes is not getting the request argument

I recently added django-axes to my Django project. It is suppose to work out the box with django-restframework. However, I am using django-rest-framework-simplejwt to handle authentication. But it should still work out the box since the only thing that is required for django-axes is passing Django's authentication method the request object which it does in it's source code (line 39 and 43).
When I try to authenticate, I get this error from django-axes:
axes.exceptions.AxesBackendRequestParameterRequired: AxesBackend requires a request as an argument to authenticate
You need to add requests to the authentication function. See sample code below.
serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def _authenticate_user_email(self, email, password, request):
# This is key: Pass request to the authenticate function
self.user = authenticate(email=email, password=password, request=request)
return self.user
def validate(self, attrs):
password = attrs.get('password')
email = attrs.get('email')
request = self.context.get('request') # <<=== Grab request
self.user = self._authenticate_user_email(email, password, request)
# All error handling should be done by this code line
refresh = self.get_token(self.user)
data = {}
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
return data
views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from authy.serializers import MyTokenObtainPairSerializer
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
urls.py
from authy.views import MyTokenObtainPairView
url(r'^/auth/api/token/$', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
It is also worth mentioning that the simple jwt lib uses the authenticate function, however it does not pass the request parameter. Therefore you need call authenticate, get_token and return data object yourself.
In addition, if you have extended the AbstractBaseUser model of django. And set the USERNAME_FIELD. Then use the param username instead of email. E.g: authenticate(username=email, password=password, request=request)
Use this:
from axes.backends import AxesBackend
class MyBackend(AxesBackend)
def authenticate(self, request=None, *args, **kwargs):
if request:
return super().authenticate(request, *args, **kwargs)
This would skip the AxesBackend in AUTHENTICATION_BACKENDS if the request is unset and would weaken your security setup.
source: https://github.com/jazzband/django-axes/issues/478

How to use Django Social Auth to connect with Instagram?

How to setup Django to authenticate with Instagram by default?
Tried django-socialauth, but no results.
Which way is the best?
Check this out:
I've found solution from sample_app.py in python-instagram :) And sync with django auth
from django.conf import settings
from django.http import HttpResponseRedirect, HttpResponse
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import render
from instagram import client
from app.models import UserProfile
unauthenticated_api = client.InstagramAPI(**settings.INSTAGRAM_CONFIG)
#login_required
def index(request):
return render(request, 'app/index.html', {})
def on_login(request):
try:
url = unauthenticated_api.get_authorize_url(scope=["likes","comments"])
return render(request, 'app/login.html', {'url': url})
except Exception as e:
print(e)
def on_callback(request):
code = request.GET.get("code")
if not code:
return "Missing code"
try:
access_token, user_info = unauthenticated_api.exchange_code_for_access_token(code)
if not access_token:
return "Could not get access token"
api = client.InstagramAPI(access_token=access_token, client_secret=settings.INSTAGRAM_CONFIG['client_secret'])
request.session['access_token'] = access_token
print "%s" % access_token
except Exception as e:
print(e)
print "%s" % user_info
print "%s" % user_info['username']
user, created = User.objects.get_or_create(username=user_info['username'])
user.backend = 'django.contrib.auth.backends.ModelBackend'
if user:
if user.is_active:
login(request, user)
return HttpResponseRedirect('/app/')
else:
return HttpResponse("Your account is disabled.")
else:
return HttpResponse("No login.")
#login_required
def on_logout(request):
logout(request)
return HttpResponseRedirect('/app/')
It's simple first redirect your users to this url
BASE_URL = "https://api.instagram.com/oauth/authorize/?"
REDIRECT_URI = "http://127.0.0.1:5000/grant-access"
url = BASE_URL + "client_id={}&redirect_uri={}&response_type=code".format(CLIENT_ID, REDIRECT_URI)
return HttpResponseRedirect(url)
Then
REQUEST_ACCESS = "https://api.instagram.com/oauth/access_token/?"
def grant_access(request):
code = request.GET.get('code')
payload = {'client_id': CLIENT_ID, 'client_secret':CLIENT_SECRET, 'grant_type':'authorization_code','redirect_uri': REDIRECT_URI, 'code': code}
resp = requests.post(REQUEST_ACCESS, data= payload)
response = json.loads(resp.text)
Now you will have the access-token save that one into db for later use and log in

Django test client http basic auth for post request

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
)