SharePoint API Call for File Upload - django

I'm building an app in Django and trying to upload a file to a SharePoint Online Site but I'm sure I've (at least) got the url wrong for the API call. I have the appropriate permissions allotted to the app in dev.microsoft.com but get back a 500 response when I try to upload.
this is the basic api call I'm trying to use
PUT /sites/{site-id}/drive/items/{parent-id}:/{filename}:/content
I'm kind of going by these 2 resources to build the url but not sure of the site-id or parent-id. For the {YourSharepointTenant} i got the tenant-id from the Azure Portal under properties. Its a long list of characters that I omitted from my code i posted here
https://www.coderedcorp.com/blog/uploading-files-to-sharepoint-with-django-and-pyth/
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content
Here is my code
def save(self, *args, **kwargs):
# Get the authenticated user credentials from office365-allauth
social = request.user.social_auth.get(provider='office365')
access_token = social.extra_data['access_token']
# build our header for the api call
headers = {
'Authorization' : 'Bearer {0}'.format(access_token),
}
# build the url for the api call
# Look at https://dev.onedrive.com/items/upload_put.htm for reference
url = 'https://{YourSharepointTenant}.sharepoint.com/sites/ITSupport/drive/root:/' + design_document + ':/content'
# Make the api call
response = requests.put(url, data=open(design_document, 'rb'), headers=headers)
return response
super(LaserMaskDesign, self).save(*args, **kwargs)

As you're targeting the graph you should use that url instead https://graph.microsoft.com/beta/sites/siteid/drive....
It is the site API that's your entry point and that will provide you with access to the drive one. Keep in mind that API is in beta so not production ready.
More documentation on this API here https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/sharepoint

Related

How to use dj-rest-auth with many clients

I'd like to have many different clients be able to access my django website (more specifically its API) but I'm not sure how to do this with django-allauth, dj-rest-auth and simplejwt.
My current client app is using the built in django template engine and is set up with django-allauth for social authentication (Google etc). It's working using the documented installation recommendations.
I would now like to create different types of clients that aren't using the django template engine (e.g. Angular, Vue, flutter mobile etc) but I'm confused how dj-rest-auth is used so that it scales to support any number of client types.
Using Google social sign in as an example, when I create a new client, I have to register a new redirect_uri specific to that client.
To test this all out, I created a simple flask app with a single link so that I can retrieve a "code/access_token" before sending it to my Django app. The link is created using the following...
var codeRequestUrl =
`https://accounts.google.com/o/oauth2/v2/auth?\
scope=email&\
access_type=offline&\
include_granted_scopes=true&\
response_type=code&\
state=state_parameter_passthrough_value&\
redirect_uri=http%3A//127.0.0.1:5000/callback&\
client_id=${clientId}`;
...and the code is retrieved at the '/callback' endpoint in flask...
#app.route("/callback", methods=['GET'])
def redirect():
code = request.args.get('code', '')
req = requests.post('http://127.0.0.1:8000/api/dj-rest-auth/google/', data={'code':code})
return "done..."
...from where I send an x-www-form-urlencoded POST request back to a dj-rest-auth endpoint that is set up as per its documentation...
class GoogleLogin(SocialLoginView):
callback_url = 'http://127.0.0.1:5000/callback'
adapter_class = GoogleOAuth2Adapter
client_class = OAuth2Client
...
urlpatterns += [
...
path('dj-rest-auth/google/', GoogleLogin.as_view(), name='google_login'),
....
]
Django then successfully returns an access_token, refresh_token and some info about the logged in user.
But this isn't something that scales well. If I were to also create an Angular client, I'd need to register a different callback (because the Angular client would be running on a different port and/or address, and I'd also need another path set up in urls.py and associate it with a new SocialLoginView subclass that can handle the different callback_url (redirect_uri).
And with all this in mind, I have no idea how to do all of this with a flutter mobile app, which as far as I'm aware, has no concept of a callback_url, so I'm not sure how making a POST request to .../dj-rest-auth/google/ would even work given that I'd instantly get a redirect_uri_mismatch error.
Have I got it backwards and the client registered at Google is the Angular, Vue, Flash etc app? That would mean that each client would have to handle its own client_id and client_secret, which then seems to bypass django-allauth's and dj-rest-auth's functionality.
I feel like I'm misinterpreting this, so I would really appreciate some suggestions.
I feel confident enough to answer my own question.
In short, yes, multiple clients (including thirdparty) is a reasonably straight forward process. Unfortunately a lot of the blog posts and tutorials that exist take the perspective of a 'second party' client, which really confuses things. The result is a lot of error messages relating to the redirect_uri.
To their credit, the Google docs for their example Flask app was exactly what I needed, but there are a couple of observations that are really important, and what caused so much confusion for me.
First, and most important, the callback (redirect_uri) is not needed in Django at all. In Django, something like this is all that is required.
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
client_class = OAuth2Client
urlpatterns += [
...
path('auth/google/', GoogleLogin.as_view(), name='google_login'),
...
]
So no callback attribute is required. The reason for this is that the Flask (or thirdparty app) handles all of the Google side authentication.
The second observation was that the redirect_uri in the Flask app seemed have have to be the same for both the "code" request step, and the "access_token" step.
You can see it in the linked example where the oauth2callback function (which handles the redirect_uri), but I've modified for use with dj-rest-auth
#app.route('/')
def index():
if 'credentials' not in flask.session:
return flask.redirect(flask.url_for('oauth2callback'))
credentials = json.loads(flask.session['credentials'])
if credentials['expires_in'] <= 0:
return flask.redirect(flask.url_for('oauth2callback'))
else:
data = {'access_token': credentials['access_token']}
headers = headers = {'Content-Type': 'application/x-www-form-urlencoded'}
r = requests.post(f'{URL_ROOT}/api/auth/google/', data=data, headers=headers)
response_json = json.loads(r.text)
access_token = response_json['access_token'] # JWT Access Token
refresh_token = response_json['refresh_token']
# Make a query to your Django website
headers = headers = {'Authorization': f'Bearer {access_token}'}
r = requests.post(f'{URL_ROOT}/api/object/{OBJECT_ID}/action/', data=data, headers=headers)
# do stuff with r
#app.route('/oauth2callback')
def oauth2callback():
if 'code' not in flask.request.args:
auth_uri = ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code'
'&client_id={}&redirect_uri={}&scope={}').format(CLIENT_ID, REDIRECT_URI, SCOPE)
return flask.redirect(auth_uri)
else:
auth_code = flask.request.args.get('code')
data = {'code': auth_code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'}
r = requests.post('https://oauth2.googleapis.com/token', data=data)
flask.session['credentials'] = r.text # This has the access_token
return flask.redirect(flask.url_for('index'))
So in summary, it's a bit like this:
On the index/home page, the user presses a "Login" html anchor that points to /login.
Flask doesnt have any credentials, so redirects to /oauth2callback to begin authentication.
First, the "code" is retrieved using Googles' GET /auth endpoint, and by using your app's client id.
The redirect_uri ensures the code flow goes back to itself, but this time, with the "code" now know, do a POST request to Google's /token endpoint using your apps's client id and client secret. Again, the redirect_uri is the same (/oauth2callback).
Now that Googles's access_token is known, the Flask app redirects back to /index (although it could be anywhere at this point)
Back in /index, the Flask app now has Google's "access_token". Use that to log into Django's dj-rest-auth endpoint that you created.
Django will then return its own access_token and refresh_token, so continue to use those as needed.
I hope this helps.
Note that your flask app will need to be registered as a new Web App with Google's OAuth2 console (so it has it's own client id and client secret). In other words, don't reuse what you may have already created with an existing Django allauth implementation (which was my scenario). Each thirdparty app maker will handle their own OAuth2 credentials.

Accessing URL parameters with python requests in an i-framed app

I am developing an Django app that that needs to consume a third party restful-API. The app itself is accessible after authentication from a portal where is exposed within an i-frame. When you select the app in the portal it redirects to the app url and appends a url parameter "?tokenid". This needs to be retrieved in order to use the third party API which needs to include the tokenid parameter as a cookie.
In my app there is a services.py module where I build all API methods to access this third party API, for instance:
url='https://www.fooapp.com'
token = request.get(url, 'tokenid')
cookies = {'session': token }
when clicking in the app link from within the main portal the response appends the tokenid parameter:
''https://www.fooapp.com/?tokenid=.eJwty8FqhDAQANB_mbMs2axmjKf-x1JkdCYyEGNxDK0s---V0uM7vBcoSzn0OGF4wb5lMRieMFXNrGUZVyq0yA6fDRznl8AA1S42UJUv5Kq26lIl36ZMRWwXO7bzo1RdKH_TeVOBdwNGax4LrTL-NXSRXJq4jeERKUjybY9eZMZpZu87xIgxCcP_NDHTrVz559rjTI-AQVzXchdSdM57h3Pn7yR9knvLGCj1kSnA-xcnjUmc.XNmOdg.hbtMmT5BkmOkhklP3JqgksDijhc''
then one of the functions to use one the API method is:
def get_foo():
url = 'https://thirdparty.com/api/v0/foo/'
r = requests.get(url, params=cookies)
data = r.json()
foos_dict = {foo['name']:foo['id'] for foo in data if 'name' in foo and 'id' in foo}
return foo_dict
then in my views I call this function etc:
class Foo(generic.TemplateView):
def get(self, request):
foo_dict = services.get_foo()
return render(request, 'foo.html', foo_dict)
I get this
HTTPSConnectionPool(host='wwww.foo.com', port=443): Max retries exceeded with url: /?tokenid (Caused by NewConnectionError('<urllib3.connection.VerifiedHTTPSConnection object at 0x000002261DFCCA20>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed',))
I also tried using
import urllib.parse as urlparse
url = 'https://wwww.foo.com/'
parsed = urlparse.urlparse(url)
token = urlparse.parse_qs(parsed.query)['?tokenid']
with no success.
Is there any other way of achieving this. I am puzzled here. What I think I don't really get is that this token id is already retrievable since I am a user inside the portal and the app is called from inside the portal, so there is no need to use request.get again (??). Any url in the iframe is appended with that ?tokenid parameter and it's valid for one hour. Any idea what could be wrong?
Many thanks in advance

Google calendar authentication page opens in terminal instead of client browser

I am integrating google calendar with my web application which is a django app. when i am doing it on localhost server, its working fine. Google authentication page opens in client browser, but when i am uploading that code to the server and integrating google calendar, then Google authentication page opens in terminal where i run my django server.
This is the page that opens for authentication in terminal
I want to provide this auth through client web browser.
`
def get_credentials(request):
creds = None
# If there are no (valid) credentials available, let the user log in.
if os.path.exists('token.pickle_' + request.GET.get('bot_id')):
with open('token.pickle_' + request.GET.get('bot_id'), 'rb') as token:
creds = pickle.load(token)
print(creds)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
CLIENT_SECRET_FILE, SCOPES)
creds = flow.run_local_server()
# Save the credentials for the next run
with open('token.pickle_' + request.GET.get('bot_id'), 'wb') as token:
pickle.dump(creds, token)
serializer = CalenderIntegrationSerializer(data={'bot_id': int(request.GET.get('bot_id')), 'status': True})
if serializer.is_valid():
serializer.save()
if os.path.exists('token.pickle_' + request.GET.get('bot_id')):
context = {'signin_url': creds}
return JsonResponse({'status': 200, 'data': 'Integration done!', 'is_integrated': True})
`
And this is my reference google calendar code python
This code is specifically for local development:
https://developers.google.com/api-client-library/python/auth/installed-app
The code gives you a hint on how to construct the URL, which you then need to send back to your user as a temporary redirect; e.g. using the redirect function. Then you need to have a django handler which accepts the redirect and executes the second half of the function. So:
Split your code into two functions.
Build a url, send a redirect with your endpoint as the callback
Google will redirect back to your endpoint after the user completes the flow.
Parse the results
Execute your code.

Django rest api Change oauth response error format

I have successfully implemented oauth2 for my django rest api project. I want to change the error response format of the login api.
Current error response is
{
"error_description": "Invalid credentials given.",
"error": "invalid_grant"
}
I want to change the error_description key to detail (detail is the key of all other django error responses). I need to make a standardize all the error responses.
Expected result is
{
"detail": "Invalid credentials given."
}
This is executing from class OAuth2Error(Exception) in /lib/python2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py file.
I would suggest intercepting this type of response and adapt it as you wish.
There are several ways, but the easiest choice would be to define your own view which is called for the authorization url, which internally calls oauth2 view and modifies response for this case, e.g. something like
from:
from oauth2_provider.views import TokenView
...
url('auth/token/', TokenView.as_view()),
to:
from oauth2_provider.views import TokenView
def custom_token_view(request, *args, **kwargs):
response = TokenView.as_view()(request, *args, **kwargs)
if "invalid_grant " in response.content:
response = do_whatever_needed(response) # i.e. response.content
return response
...
url('auth/token/', custom_token_view),
More flexible/general solution alternative
If you use Django rest framework (DRF) I would suggest:
setting up custom DRF JSON renderer - define your own,
your custom renderer should inherit default renderer (rest_framework.renderers.JSONRenderer)
in renderer intercept all responses - by calling default render function and detect this particular one (error=invalid_grant) and customize it
If you don't use DRF:
create custom middleware
if django < 1.10 then check implementing process_response, if django >=1.10 then call method
again, intercept all responses and detect only this one, which you can modify as you need

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})