How to include Oauth2 auth urls in Swagger? - python

I use Django drf-spectacular OAuth Toolkit for an Oauth2 password flow. Unfortunately, Swagger doesn't recognize the auth URLs.
This is my urls.py
urlpatterns = [
# schema
path("api/schema/", SpectacularAPIView.as_view(api_version='v1'), name="schema"),
path(
"api/schema/swagger/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
path(
"api/schema/redoc/",
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
path("api/oauth/", include("apps.main.oauth.urls", namespace="oauth2_provider")),
]
How can I fix thit?

To make it available to swagger you have to override the Oauth API, for example, override the token API and and write an inline serializer in the #extend_schema and pass the post method.
from drf_spectacular.utils import extend_schema, inline_serializer
from oauth2_provider.views.application import TokenView
class TokenApiView(TokenView, APIView):
#extend_schema(
request=inline_serializer(
name="InlineTokenSerializer",
fields={
"username": serializers.CharField(),
"password": serializers.CharField(),
"grant_type": serializers.CharField(required=False),
"Scope": serializers.CharField(required=False),
"client_id": serializers.CharField(),
},
)
)
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)

The oauth toolkit does provides regular html views, which are not DRF views. Therefore they do not appear in the schema because spectacular can only parse any of the DRF-type views.
What you need to do is add some settings that direct SwaggerUI to those auth views:
SPECTACULAR_SETTINGS = {
# Oauth2 related settings. used for example by django-oauth2-toolkit.
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#oauth-flows-object
'OAUTH2_FLOWS': [],
'OAUTH2_AUTHORIZATION_URL': None,
'OAUTH2_TOKEN_URL': None,
'OAUTH2_REFRESH_URL': None,
'OAUTH2_SCOPES': None,
# other spectcular settings
}
So 3 steps are basically required to make this fully functional:
Add the views to urlpatterns for oauth2_provider (as you did)
Make sure the views have OAuth2Authentication and the corresponding permission_classes (directly or via DRF default setting)
Add the settings as outlined above. You may not need all of them depending on the supported flows you have to set at least some URLs there.

Related

DRF: request.user returns nothing for token based authentication

I am basically new to DRF & trying to make one API endpoint to update user via mobile app,here is my code for that,
class MobileAppUserUpdateView(APIView):
def post(self, request, format=None):
user = request.user
print('USER',user) #<- prints nothing
print('TOKEN',request.auth) #<- prints the token
return ResponseBuilder.success([constants.RECORD_UPDATED])
there I want to access the user who has made that request using the auth token but getting nothing there.Here is my settings for DRF,
NON_FIELD_ERRORS_KEY = 'message'
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
# 'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
'NON_FIELD_ERRORS_KEY': NON_FIELD_ERRORS_KEY,
}
I have also added the "rest_framework.authtoken" in the INSTALLED_APPS list as per the documentation.Any help would be appreciated...
Just few things more..
You need to add authentication_classes in MobileAppUserUpdateView which includes TokenAuthentication, like this:
class MobileAppUserUpdateView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, format=None):
...
Then make sure you have added rest_framework.authtoken in your INSTALLED_APPS settings:
INSTALLED_APPS = [
...
'rest_framework.authtoken'
]
Then run manage.py migrate after changing your settings.
Use a debugger to debug the code and check what your request object is receiving.
You can follow this doc for more.
The problem was in the users table, User model returns the email for str method & the email field had null value that's why I was getting nothing when I try to print it.

Integrating django-axes with customized django rest framework and simpleJWT

I am a beginner at Django and was building a login system. I wanted to use axes with django rest framework and simpleJWT.
According to the documentation for Django-Axes:
Modern versions of Django REST Framework after 3.7.0 work normally with Axes out-of-the-box and require no customization in DRF.
Now I have built a Custom User for my project who gets authenticated using my own custom authentication backend. For creating the Access and the Refresh tokens I have localhost:8000/api/loginend point which creates both the tokens and stores them as HTTPOnly cookie.
The login view is like this :
#api_view(['POST'])
#permission_classes([AllowAny])
#ensure_csrf_cookie
def login_view(request):
# print("captcha token = ", request.POST.get('g-recaptcha-response'))
Account = get_user_model()
EmailId = request.data.get('EmailId')
password = request.data.get('password')
response = Response()
if (EmailId is None) or (password is None):
raise exceptions.AuthenticationFailed('username and password required')
# get the user with the credentials provided
user = Account.objects.filter(EmailId=EmailId).first()
if not check_user_validity(user, password):
# locking the user
raise exceptions.AuthenticationFailed('The credentials are invalid')
else:
print('user is valid')
# get the token values
tokenvals = get_jwt_tokens(user)
response.set_cookie(key='refreshtoken', value=tokenvals['refresh_token'], httponly=True)
response.set_cookie(key="accesstoken", value=tokenvals['access_token'])
response.data = {
'access_token': tokenvals['access_token'],
'id': user.id
}
return response
I can refresh the access token using localhost:8000/api/Refresh endpoint.
get_jwt_token(user) returns a dictionary with both access and refresh tokens created in a customized way. It doesn't make use of any JWT Serializers or Views.
For the Login View I want to use axes to block users upon multiple invalid logins. But I have no clue how I could proceed from here on.
I followed the documentation for axes and modified the settings file like it was mentioned.
AUTHENTICATION_BACKENDS = [
# AxesBackend should be the first backend in the AUTHENTICATION_BACKENDS list.
'axes.backends.AxesBackend',
# Django ModelBackend is the default authentication backend.
'django.contrib.auth.backends.ModelBackend',
]
MIDDLEWARE = [
..
..
..
# at the end
'axes.middleware.AxesMiddleware',
]
INSTALLED_APPS = [
..
..
..
"authentication",
"axes",
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# a custom authentication method
'authentication.authentication.SafeJWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', # make all endpoints private
)
}
How can I proceed from this point onwwards? I couldn't really figure out much since I have done plenty customization..
Also Should I include 'django.contrib.auth.backends.ModelBackend', in the Backend since I am using a custom authentication?
How can I proceed from this point onwwards?

Django Rest Framework - Using Session and Token Auth

I am trying to get this working but don't know if this is possible. It should be doing it like this.
I developed a web app using Django + Rest-Framework + jQuery, and I want to have an external application to consume the same REST API, using JWT Tokens for authentication.
My current config is like this.
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_RENDERER_CLASS': [
'rest_framework.renderers.JSONRenderer',
]
}
SIMPLE_JWT = {
'AUTH_HEADER_TYPES': ('Bearer',),
}
views.py
class ListFileView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
user = request.user
if user:
obj = Foo.objects.filter(owner=user)
serializer = FooSerializer(obj, many=True)
data = serializer.data
return Response(data, status=status.HTTP_200_OK)
return Response({'detail': 'You have no access to files.'}, status=status.HTTP_400_BAD_REQUEST)
The tricky part is that when I use:
permission_classes = (IsAuthenticated,)
I can perform ajax calls from an external application (using a valid JWT token), but jQuery calls from within the app (with an authenticated user) fail with:
{"detail":"Authentication credentials were not provided."}
And on the other hand, if I use autentication_classes instead of permission_classes:
authentication_classes = (SessionAuthentication, BasicAuthentication)
I can perform ajax calls from within the web app using jQuery, but external calls fail with the same 403 error.
I tried using both like this:
class ListFileView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
...
but the external calls are also rejected.
Is it possible to have these two types of Auth working together in the same class view, or should I separate into two endpoints?
EDIT
Example calls from within the app with jQuery:
<script type="text/javascript">
function loadTblDocs() {
$("#tbl-docs > tbody").empty();
$.ajaxSetup({
headers: { "X-CSRFToken": '{{csrf_token}}' }
});
$.ajax({
type: 'GET',
contentType: "application/json; charset=utf-8",
url: "/api/list/",
dataType: "json",
success: function (data) {
console.log(data);
}
});
};
</script>
And externally via VBA code:
Set web = CreateObject("WinHttp.WinHttpRequest.5.1")
web.Open "GET", "/api/list/", False
web.SetRequestHeader "Authorization", "Bearer <JWT_TOKEN_HERE>"
web.Send
I couldn't work out exactly what is going on in your case, because the behavior in the 3 cases you shared does not seem to be consistent, but going to explain simply how authentication and authorization is determined in DRF, this might help you figure the issue out.
You should be able to use two authentication classes with no problems. With DRF, authentication works like this:
At each request, DRF goes over the provided authentication classes, in the order they are defined. For each class, there are 3 cases:
If it can authenticate the request with the current class, DRF sets request.user. From this point on, this request is authenticated.
If no authentication credentials are present, DRF skips that class
If authentication credentials are present but invalid, such as an invalid JWT token in Authorization header, DRF raises an exception and returns a 403 response.
DRF views normally use the DEFAULT_AUTHENTICATION_CLASSES variable defined in the settings file, but if you provide them in a view, settings are overridden.
Authorization comes into play when you add permission_classes to your views. permission_classes control access to your views, so when you add IsAuthenticated to a view's permission classes, DRF rejects the request with a 403 response if you try to access that view without an authenticated user.
So in your initial case, your internal AJAX request failing in this case suggests that there is no authenticated user data in your request session. I do not know what could be causing this. Perhaps you do not update request session in your login view (Django's login method should do this automatically).
In your second case, you remove permission_classes from the view, so the view will serve the request regardless if there is an authenticated user in the request or not. So it is expected that your internal AJAX request succeeds here, but I do not know why your external request fails in this case.
In your third case, from the point of your internal AJAX request, the scenario seems to be the same as the first case, so I do not know why your internal AJAX request succeeds in this case but not in the first case. The external request failing here is expected because you added the IsAuthenticated permission class to the view, but did not include JWTAuthentication in the authentication_classes, so your request can not be authenticated with your JWT token here.

Django REST Framework Web Login not working

I can't seem to figure out why my rest framework login in my django project won't work... I have got the log in link there and all and it does redirect me to the rest login page but after filling in my credentials and clicking login it just takes me back to the page I came from but it does not seem to authenticate me as it still says "Login" in the top right corner...
I would expect it to change to "admin" in this case as I am trying to login using my superuser named "admin"...
I am surely missing something..
Here are my REST Urls
path("rest/auctions", auction_list, name="auction-list"),
path('rest/auctions/<int:pk>', auction_detail, name="auction-detail"),
path('rest-auth/', include('rest_framework.urls', namespace='rest_framework')),
The rest-auth/ one I know is the one that somehow adds the login button and all (Magic?) But maybe I have to do something else too to make it authenticate? I don't know... I am using the built in django authentication system and User model.
views.py
#api_view(['GET'])
def auction_list(request, format=None):
if request.method == 'GET':
query = request.GET.get('q')
if query is not None:
auctions = Auction.objects.filter(Q(title__icontains=query), deadline__gte=timezone.now(),
status=Auction.STATUS_ACTIVE)
else:
auctions = Auction.objects.all()
serializer = AuctionSerializer(auctions, many=True, context={'request': request})
return Response(serializer.data)
serializer.py
class AuctionSerializer(serializers.ModelSerializer):
winning_bid = serializers.SlugRelatedField(many=False, read_only=True, slug_field='amount', allow_null=True)
seller = serializers.SlugRelatedField(many=False, read_only=True, slug_field='username', allow_null=False)
class Meta:
model = Auction
fields = ('id', 'url', 'title', 'description', 'deadline', 'min_price', 'winning_bid', 'seller')
in my Settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
],
}
Lastly I can mention that the authentication works when I use the API via Postman.
If the other solution doesn't work, add this line too
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
),
You should remove BasicAuthentication from the settings.py and it should work as expected. BasicAuthentication works by base64 encoding the user login information and attaching them to HTTP Authorization Header. That's why your requests via Postman work normally.
By default Django uses Session based authentication, which depends on cookies on the client to store the user session information once the user is logged in. Obviously username and password are not stored in cookie with BasicAuthentication.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly'
],
}

How to use the OAuth2 toolkit with the Django REST framework with class-based views?

I'm trying to add an API using the Django REST framework to an existing codebase which uses the Django OAuth2 toolkit. The existing views make use of the fact that the OAuth2 toolkit's backend modifies the behavior of Django's login_required decorator so that it uses OAuth2 authentication. The function-based views look similar to the one in the tutorial example:
from django.contrib.auth.decorators import login_required
from django.http.response import HttpResponse
#login_required()
def secret_page(request, *args, **kwargs):
return HttpResponse('Secret contents!', status=200)
I'm trying to adapt the example given on https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html to my situation, but I'm finding it's not an exact correspondence, because the example uses the DRF's ModelViewSet classes whereas my current view uses a (less-integrated) generic class-based view:
from rest_framework import generics
from ..models import Session
from ..serializers import SessionSerializer
class SessionDetail(generics.UpdateAPIView):
queryset = Session.objects.all()
serializer_class = SessionSerializer
where I've set the default permission class to IsAuthenticated in settings.py:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
This should allow a Session object to be updated using PATCH requests, but it appears to return a 403 Forbidden response for use cases similar to the ones for the current views decorated with login_required.
How can I update the SessionDetail view so that it behaves the same as a function view decorated with login_required? I suspect that I need to use the TokenHasReadWriteScope permission class, since this appears to be the only one for which the required_scopes is optional. (The views decorated with login_required don't provide required_scopes either).
Would it be like this:
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from oauth2_provider.contrib.rest_framework import OAuth2Authentication, TokenHasReadWriteScope
from ..models import Session
from ..serializers import SessionSerializer
class SessionDetail(generics.UpdateAPIView):
authentication_classes = [OAuth2Authentication]
permission_classes = [TokenHasReadWriteScope]
queryset = Session.objects.all()
serializer_class = SessionSerializer
Also, would I have to update my REST_FRAMEWORK setting with a DEFAULT_AUTHENTICATION_CLASSES like this:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
)
}
Right now if I run the server and try to make a patch request with the browsable API, I get a 401 Unauthorized error:
Also if I Log in using a username and password that I just created using python manage.py createsuperuser, it goes straight back to this 401 page instead of one with a form to make a PATCH request as I'd expect.
Any ideas on how to fix this?

Categories