8

Is it possible to upload a file to the Shared Documents library of a Microsoft SharePoint site with the Python OneDrive SDK?

This documentation says it should be (in the first sentence), but I can't make it work.

I'm able to authenticate (with Azure AD) and upload to a OneDrive folder, but when trying to upload to a SharePoint folder, I keep getting this error:

"Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."

The code I'm using that returns an object with the error:

(...authentication...)
client = onedrivesdk.OneDriveClient('https://{tenant}.sharepoint.com/{site}/_api/v2.0/', auth, http)
client.item(path='/drive/special/documents').children['test.xlsx'].upload('test.xlsx')

where I'd like to upload on the web

I can successfully upload to https://{tenant}-my.sharepoint.com/_api/v2.0/ (notice the "-my" after the {tenant}) with the following code:

client = onedrivesdk.OneDriveClient('https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http)
returned_item = client.item(drive='me', id='root').children['test.xlsx'].upload('test.xlsx')

How could I upload the same file to a SharePoint site?

(Answers to similar questions (1,2,3,4) on Stack Overflow are either too vague or suggest using a different API. My question is if it's possible using the OneDrive Python SDK, and if so, how to do it.)


Update: Here is my full code and output. (Sensitive original data replaced with similarly formatted gibberish.)

import re
import onedrivesdk
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest

# our domain (not the original)
redirect_uri = 'https://example.ourdomain.net/' 
# our client id (not the original)
client_id = "a1234567-1ab2-1234-a123-ab1234abc123"  
# our client secret (not the original)
client_secret = 'ABCaDEFGbHcd0e1I2fghJijkL3mn4M5NO67P8Qopq+r=' 
resource = 'https://api.office.com/discovery/'
auth_server_url = 'https://login.microsoftonline.com/common/oauth2/authorize'
auth_token_url = 'https://login.microsoftonline.com/common/oauth2/token'
http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http_provider=http, client_id=client_id, 
                                auth_server_url=auth_server_url, 
                                auth_token_url=auth_token_url)

should_authenticate_via_browser = False
try:
    # Look for a saved session. If not found, we'll have to 
    # authenticate by opening the browser.
    auth.load_session()
    auth.refresh_token()
except FileNotFoundError as e:
    should_authenticate_via_browser = True
    pass

if should_authenticate_via_browser:
    auth_url = auth.get_auth_url(redirect_uri)
    code = ''
    while not re.match(r'[a-zA-Z0-9_-]+', code):
        # Ask for the code
        print('Paste this URL into your browser, approve the app\'s access.')
        print('Copy the resulting URL and paste it below.')
        print(auth_url)
        code = input('Paste code here: ')
        # Parse code from URL if necessary
        if re.match(r'.*?code=([a-zA-Z0-9_-]+).*', code):
            code = re.sub(r'.*?code=([a-zA-Z0-9_-]*).*', r'\1', code)
    auth.authenticate(code, redirect_uri, client_secret, resource=resource)
    # If you have access to more than one service, you'll need to decide
    # which ServiceInfo to use instead of just using the first one, as below.
    service_info = ResourceDiscoveryRequest().get_service_info(auth.access_token)[0]
    auth.redeem_refresh_token(service_info.service_resource_id)
    auth.save_session()  # Save session into a local file.

# Doesn't work
client = onedrivesdk.OneDriveClient(
    'https://{tenant}.sharepoint.com/sites/{site}/_api/v2.0/', auth, http)
returned_item = client.item(path='/drive/special/documents')
                      .children['test.xlsx']
                      .upload('test.xlsx')
print(returned_item._prop_dict['error_description'])

# Works, uploads to OneDrive instead of SharePoint site
client2 = onedrivesdk.OneDriveClient(
    'https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http)
returned_item2 = client2.item(drive='me', id='root')
                        .children['test.xlsx']
                        .upload('test.xlsx')
print(returned_item2.web_url)

Output:

Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown.
https://{tenant}-my.sharepoint.com/personal/user_domain_net/_layouts/15/WopiFrame.aspx?sourcedoc=%1ABCDE2345-67F8-9012-3G45-6H78IJKL9M01%2N&file=test.xlsx&action=default
9
  • Can you provide the full traceback? Or specify which line was the source of the error the line starting with client or returned_item? Commented Oct 13, 2016 at 19:28
  • Also, have you set the appropriate AuthScope for SharePoint Online? This is separate from your OneDrive permissions. Commented Oct 13, 2016 at 19:44
  • @Gator_Python - I added the full code. There is no error thrown, but the returned object contains the properties of the uploaded file in the successful case, and this error message in the unsuccessful case. The AuthScope is set like that (in fact, all the permissions are ticked). Commented Oct 14, 2016 at 11:02
  • I noticed you did not specify the drive when you tried doing this for your sharepoint library. It may seem counterintuitive, but a sharepoint document library is considered a drive and is accessed the same way you access other OneDrive resources. Whether you're accessing OneDrive or Sharepoint, API actions should (pretty much) always address a drive resource. See drive resources and list drives Commented Oct 17, 2016 at 14:27
  • Thanks! I realized I can list drives with onedrivesdk.OneDriveClient('https://{tenant}-my.sharepoint.com/_api/v2.0/', auth, http).drives.get(). It lists one drive, with type 'business'. I assume this is the OneDrive storage. For the URL without -my, I can execute onedrivesdk.OneDriveClient('https://{tenant}.sharepoint.com/_api/v2.0/', auth, http).drive.get(), which returns an object with the same error message (AudienceUri...) as above. Commented Oct 18, 2016 at 15:14

1 Answer 1

5

I finally found a solution, with the help of (SO user) sytech.

The answer to my original question is that using the original Python OneDrive SDK, it's not possible to upload a file to the Shared Documents folder of a SharePoint Online site (at the moment of writing this): when the SDK queries the resource discovery service, it drops all services whose service_api_version is not v2.0. However, I get the SharePoint service with v1.0, so it's dropped, although it could be accessed using API v2.0 too.

However, by extending the ResourceDiscoveryRequest class (in the OneDrive SDK), we can create a workaround for this. I managed to upload a file this way:

import json
import re
import onedrivesdk
import requests
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest, \
    ServiceInfo

# our domain (not the original)
redirect_uri = 'https://example.ourdomain.net/' 
# our client id (not the original)
client_id = "a1234567-1ab2-1234-a123-ab1234abc123"  
# our client secret (not the original)
client_secret = 'ABCaDEFGbHcd0e1I2fghJijkL3mn4M5NO67P8Qopq+r=' 
resource = 'https://api.office.com/discovery/'
auth_server_url = 'https://login.microsoftonline.com/common/oauth2/authorize'
auth_token_url = 'https://login.microsoftonline.com/common/oauth2/token'

# our sharepoint URL (not the original)
sharepoint_base_url = 'https://{tenant}.sharepoint.com/'
# our site URL (not the original)
sharepoint_site_url = sharepoint_base_url + 'sites/{site}'

file_to_upload = 'C:/test.xlsx'
target_filename = 'test.xlsx'


class AnyVersionResourceDiscoveryRequest(ResourceDiscoveryRequest):

    def get_all_service_info(self, access_token, sharepoint_base_url):
        headers = {'Authorization': 'Bearer ' + access_token}
        response = json.loads(requests.get(self._discovery_service_url,
                                           headers=headers).text)
        service_info_list = [ServiceInfo(x) for x in response['value']]
        # Get all services, not just the ones with service_api_version 'v2.0'
        # Filter only on service_resource_id
        sharepoint_services = \
            [si for si in service_info_list
             if si.service_resource_id == sharepoint_base_url]
        return sharepoint_services


http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http_provider=http, client_id=client_id,
                                auth_server_url=auth_server_url,
                                auth_token_url=auth_token_url)

should_authenticate_via_browser = False
try:
    # Look for a saved session. If not found, we'll have to
    # authenticate by opening the browser.
    auth.load_session()
    auth.refresh_token()
except FileNotFoundError as e:
    should_authenticate_via_browser = True
    pass

if should_authenticate_via_browser:
    auth_url = auth.get_auth_url(redirect_uri)
    code = ''
    while not re.match(r'[a-zA-Z0-9_-]+', code):
        # Ask for the code
        print('Paste this URL into your browser, approve the app\'s access.')
        print('Copy the resulting URL and paste it below.')
        print(auth_url)
        code = input('Paste code here: ')
        # Parse code from URL if necessary
        if re.match(r'.*?code=([a-zA-Z0-9_-]+).*', code):
            code = re.sub(r'.*?code=([a-zA-Z0-9_-]*).*', r'\1', code)

    auth.authenticate(code, redirect_uri, client_secret, resource=resource)
    service_info = AnyVersionResourceDiscoveryRequest().\
        get_all_service_info(auth.access_token, sharepoint_base_url)[0]
    auth.redeem_refresh_token(service_info.service_resource_id)
    auth.save_session()

client = onedrivesdk.OneDriveClient(sharepoint_site_url + '/_api/v2.0/',
                                    auth, http)
# Get the drive ID of the Documents folder.
documents_drive_id = [x['id']
                      for x
                      in client.drives.get()._prop_list
                      if x['name'] == 'Documents'][0]
items = client.item(drive=documents_drive_id, id='root')
# Upload file
uploaded_file_info = items.children[target_filename].upload(file_to_upload)

Authenticating for a different service gives you a different token.

Sign up to request clarification or add additional context in comments.

4 Comments

The code looks good, but the code I'm writing needs to be automated, so I cannot copy and paste the "code" generated by browser each time, this "code" will expire very soon too. So, based on your code, I modified my code, but no matter it's v1.0 or v2.0, I still get empty response['value'].....May I ask, do you know what's wrong with my code? Here's my code: github.com/hanhanwu/Basic_But_Useful/blob/master/…
@CherryWu - The code above is automated. auth.save_session() saves the authentication information into a file called session.pickle. auth.load_session() loads this. The way it works is that the first time you run this code, it will open a browser window where you need to log in and copy & paste the URL after redirection back into the Python console. This authentication token is then saved, and next time loaded without a browser opening. You can read more about this at the "Saving and Loading a Session" section here.
Thank you very much Attila, today I don't have time to try this, but a quick question, the code we got from the browser has very short life span, does this mean, with the "Saving and Loading a Session" in your code, that "code" will no longer expire?
I don't know, I haven't run into the expiry yet. The short life span token is the access token, what you store here is also the refresh token. According to this answer here, if used at least once in every 14 days, these Azure AD tokens should expire in 90 days.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.