Flask-praetorian, not able to pass auth header to protected endpoint - flask

im using flask-praetorian in order to add security to my app.
I got two routes: one for /login and a protected endpoint called /profile. The Login route works fine, it takes the username and password from the form, im able to pass the information to the guard object and authenticate it to get a new token, but im not been able to pass this token to the request headers for the protected endpoint.
I've tried to use the 'session" to add the header, the 'make_response' method, and the redirect(url_for()), but everytime it gets to the endpoint does it without the correct header causing the error.
Code below:
#user.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if request.method == 'POST':
username = form.username.data
password = form.password.data
user = guard.authenticate(username, password)
token = guard.pack_header_for_user(user)
resp = make_response(profile())
resp.headers['Authorization'] = token['Authorization']
return resp
else:
return render_template('login.html', form=form)
#user.route('/profile')
#auth_required
def profile():
return render_template('profile.html')

(Author of the flask-praetorian package here)
The issue here is that you are just calling the profile() method from your login() method. Calling pack_header_for_user will not actually put the correct auth headers in the current request context. Instead, it just creates a header dict that could be put into a request (but is nicely returned as json). You could jam the token into the current request by tinkering with flask's request context, but it would not be the best way.
The right way to do this is to have your login route include the token in your response payload and then to make another call from your frontend code to the profile endpoint with the token added to the request header.
Really, I think you would be better off going with a different package like flask-security. The flask-praetorian package is intended to be used with pure APIs, and I haven't done any testing or prototyping with standard flask pages.

Related

Django: reencode request and call another view

I am working on a backend application that exposes a JSON-based REST api. However, I am using some library that has some of its own endpoints and they accept form-endcoded data. I want to extend the actions done by those endpoints and expose my extended versions. At the same time, I want my API to be consistent and I want all my endpoints to use JSON. To be more specific, I use the oauth2_provider library, and I want to logout users when they revoke a token. I am also considering making a logout handle that would require the bearer token in the Authorization header and would both logout the user and revoke the token.
My first approach to this was to write a wrapper view around the oauth2_toolkit revoke token view, loggging the user out in the wrapper view and then calling the actual revoke_token view. However, I have to modify the body of the request, which is immutable.
class Logout(View):
def get(self, request):
if request.user.is_authenticated:
logout(request)
# modify the .body attr of the request or create a new request here
RevokeTokenView.as_view(request)
I couldn't find a way to clone a Django request or modify it. Is there a way to do it?
(For now I am looking into creating a custom oauthlib_backend_class, but it feels as a bit of an overkill)
UPD: the data that is required by the revoke_token view is in the request_body
You can modify the immutable request body like below:
class Logout(View):
def get(self, request):
if request.user.is_authenticated:
logout(request)
# modify or create a new request here
body = request.GET.copy()
body['key'] = 'new_value'
request.GET = body
RevokeTokenView.as_view(request)
UPDATE: If you want to change .body attribute of the request object, according to django's implementation, you should change the ._body attribute which is a private attribute of the request object.

Django - RawPostDataException: You cannot access body after reading from request's data stream

I'm really stuck at this problem for a couple of days now.
While I understand, what's happening here, I don't really know the best workaround/solution for this.
Problem:
I'm trying to create a user login endpoint using Django and DRF in general.
My login API needs to support a login via password as well as login via OTP.
My LoginView looks like:
def post(self, request, **kwargs):
"""
post
Method to handle user login
:param request:
:param args:
:param kwargs:
:return:
"""
request_data = request.data
login_using_password = request_data.get('login-with-password') is True
login_using_otp = request_data.get('login-with-otp') is True
if request_data is not None:
if all((login_using_password, login_using_otp)):
raise accounts_exceptions.InvalidLoginRequestError()
if login_using_password:
return Response(self._login_with_password(request))
elif login_using_otp:
return Response(self._login_with_otp(request))
raise accounts_exceptions.InvalidLoginRequestError()
return Response(self._login_with_password(request))
Also my _login_with_password looks like:
def _login_with_password(self, request, **kwargs):
"""
_login_with_password
A utility method to handle login with password
:param request:
:return:
"""
return getattr(ObtainJSONWebToken.as_view()(request=request._request, ), 'data')
When I try to login, Django complains saying RawPostDataException You cannot access body after reading from request's data stream
I'm using JWT to authenticate requests. ObtainJSONWebToken is a view provided by DRF-JWT to obtain access tokens to authenticate requests.
What is the workaround/solution for this?
Is there a better way to support such a login requirement?
Thanks in advance!
Resolved this.
There's no concrete way to solve the problem above.
Django disallows access to request.data multiple times.
It could be done only once for the entire request lifetime.
So, this left me with two solutions:
Move my request payload to query params.
Move my request payload to url context.
I ended up using a mix and match of both.
So, basically I used request.query_params and self.context to fetch data from the request and changed my URL and request structure accordingly.

django-rest-auth: social login with google

The django-rest-auth documentation discusses Facebook integration, which I am not interested in-- my concern is providing social login via Google. I have tried this for quite some time now and I'm wondering if anyone else has any documentation on how they did this...even just a rough sketch would be helpful. So far, I have not found any results for this search. I am almost there, but cannot get it to work with the Django rest framework (DRF) browsable API.
Here is what I have so far:
I started from the demo project supplied on the django-rest-auth github page and modified the social login template HTML page to only require the 'code' input, not both 'code' AND 'access_token'. When I supply a valid code (obtained by a separate request to google's auth endpoint), this works fine; the browsable API renders the usual web page with the 'key' (my application's API token for the user) in the response. Checking the django admin, everything worked- the user is logged in, email is authenticated, etc. Good so far.
The issue is that starting point of supplying the 'code'- and how I get that code from google in the first place. When I previously (successfully) used the allauth package, I could simply click on a link, which would "invisibly" perform the whole OAuth2 flow (i.e. request the code, use that code to get the access token, and use the access token to get user's google account info).
To recreate that seamless flow (i.e. NOT starting out with the code), I figured I could interrupt the OAuth2 flow and "intercept" the code returned from google, and then POST that code to the rest-auth social login API. To that end, I created a custom allauth.socialaccount.providers.oauth2.views.OAuth2CallbackView by overriding the dispatch method:
class CustomOAuth2CallbackView(OAuth2CallbackView):
def dispatch(self, request):
# gets the code correctly:
code = request.GET['code']
# rp is of type requests.methods.Response
rp = requests.post(<REST-AUTH API ENDPOINT>, data = {'code':code})
return rp
Usually, this method is called when google sends a GET request to the callback uri you initially supply to google's auth endpoint. With this override, I am able to successfully parse the code returned from google in that callback. The POST request works and has the user's key in the resp._content field. However, it ultimately fails to produce the intended view in the DRF browsable API.
Based on diving down in the callstack, I find that rest_framework.views.APIView.dispatch returns an object of type rest_framework.response.Response. However, when the requests.post method used above completes, it returns an instance of type requests.models.Response. As a result, it does not have the proper attributes and fails in the django middleware. For example, it has no acceptable_renderer attribute and no 'get' method (which is used in django.middleware.clickjacking.py). I could, conceivably, add these requirements to the requests.models.Response (rp) instance, but then this hack becomes even more of a kludge.
Thanks for any help you can provide!
https://github.com/st4lk/django-rest-social-auth
class SocialLoginSignup(SocialSessionAuthView):
"""
Mobile user social signup and login api view
args:
provider: name of the social network
access_token: auth token got from the social sites
"""
serializer_class = SocialSignUpSerializer
authentication_classes = (TokenAuthentication,)
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
provider_name = serializer.validated_data['provider']
decorate_request(request, provider_name) # assign the provider class object in request
authed_user = request.user if not request.user.is_anonymous() else None
token = serializer.validated_data['access_token']
if self.oauth_v1() and request.backend.OAUTH_TOKEN_PARAMETER_NAME not in serializer.validated_data:
request_token = parse_qs(request.backend.set_unauthorized_token())
return Response(request_token)
try:
# authentication function get call from assign class object in request
user = request.backend.do_auth(token, user=authed_user)
except social_exceptions.AuthException as e:
raise exceptions.ParseError({'error':str(e)})
except social_exceptions.AuthTokenError as e:
raise exceptions.ParseError({'error': str(e)})
except social_exceptions.AuthAlreadyAssociated as e:
raise exceptions.ParseError({'error': str(e)})
except social_exceptions.AuthFailed as e:
raise exceptions.ParseError({'error':str(e)})
except social_exceptions.AuthUnknownError as e:
raise exceptions.ParseError({'error': str(e)})
except social_exceptions.WrongBackend as e:
raise exceptions.ParseError({'error':str(e)})
except Exception as e:
raise exceptions.ParseError({'error': social_message.INVALID_AUTH_TOKEN})
token, created = Token.objects.get_or_create(user=user)
return Response({'auth_token':token.key})

Django comments CSRF error

Getting a csrf error I cant figure out how to fix, i have rest auth working, user is able to update their details like so:
but with Django Comments i get this csrf error using the same csrf token Error:
I would like to get rid of this error on the /comments/post/ endpoint, such that this endpoint behaves similar to /rest-auth/user/ view which accepts an "Authorization: Token 792b5fb27b4fe805e895c91274f26b6ab13cb654" header field to relevant provide data to the authenticated user.
The following is an exert of the csrf related decotaros on the respective views shown in the screen shots:
From the /comments/post/ endpoint
#csrf_protect
#require_POST
def post_comment(request, next=None, using=None):
# Fill out some initial data fields from an authenticated user, if present
data = request.POST.copy()
if request.user.is_authenticated():
if not data.get('name', ''):
data["name"] = request.user.get_full_name() or request.user.get_username()
if not data.get('email', ''):
data["email"] = request.user.email
From the /rest-auth/user/ endpoint
#api_view(['GET'])
#permission_classes((IsAuthenticated, ))
def get_user(request, **kwargs):
pk = request.data['pk']
user = MyUser.objects.get(pk=pk)
serializers = UsersSerializer(user)
return Response(serializers.data)
You're using the wrong content type. Please change it into application/json and try again.
The decorators for your endpoints are different, thus you need to adjust the headers accordingly.
For your /rest-auth/ view the WWW-Authenticate header is required as mentioned here.
The comments view /comments/ endpoint has the csrf_protect decorators which means that the header must match the csrf-token returned in the cookie,as Fede mentions in your header you only require 'X-CSRFToken' with the matching value from the cookie.
I think you are using django-rest-framework which comes with the csfr token exempt by default, but postman is sending a csfr token that is why you are getting that error.
cleaning the cookies might solve the problem.

Tastypie requests showing Anonymous user even after Logging In

I am working on a project in angularjs and django 1.6
login(request, user)
print(request.user, request.user.id)
The login is done using standard login from django.contrib.auth in the LoginResource. Inside that it successfully prints user and its id.
Then after logging in i tried another api request from client side.Say there is a MessageResource model. And it has archive function.
def archive(self, request, **kwargs):
self.method_check(request, allowed=['post'])
data = self.deserialize(request, request.body,format=request.META.get('CONTENT_TYPE', 'application/json'))
arch_data = data.get('arch_list', '')
print(request.user, request.user.id)
So i tried to track the user inside this function.But
the last print line prints
AnonymousUser None
Means the user data is not stored in it. Can someone tell me what i'm doing wrong ? How can i get the login user from request?
Cause one - authentication that doesn't attach user to request
It depends of authorization in particular resource.
Let say if you are using ApiKeyAuthorization the user is fetched from credentials and assigned to the request during execution of authentication's is_authenticated method here: https://github.com/toastdriven/django-tastypie/blob/master/tastypie/authentication.py#L206
But if you are using standard Authentication, the method is_authenticated doesn't check or assign anything: https://github.com/toastdriven/django-tastypie/blob/master/tastypie/authentication.py#L47. So event if there are credentials in request they wont be checked or assigned.
Cause two - using resource extended methods doesn't provide authentication etc.
Yes in this case you have to check on your own: allowed http methods authorization, authentication, serialize request.body etc.
If your authentication assign user to request in is_authenticated method like ApiKeyAuthentication does for instance. You just add one line:
def archive(self, request, **kwargs):
self.method_check(request, allowed=['post'])
self.is_authenticated(request)
data = self.deserialize(request, request.body,format=request.META.get('CONTENT_TYPE', 'application/json'))
arch_data = data.get('arch_list', '')
print(request.user, request.user.id)