Open adevine opened 4 months ago
I don't know if these issues are still being reviewed, but for the benefit of others, documenting here how I eventually got both consumer and multitenant business accounts working. For what it's worth, there were two issues that I literally spent many hours trying to figure out the right configuration to get things working, and I think it would be well worth it to more clearly and explicitly document the solutions to these issues in the OneDrive Picker doc. These 2 issues were:
For the first issue, note that for clarification I wanted to implement something very similar to what ChatGPT recently added to their website: the ability to connect a OneDrive account (either a consumer or a business account) and open the Picker to select files that can then be uploaded to the app (in that case ChatGPT). The thing I find honestly unhelpful about the docs and examples is how it specifies the baseUrl
, and how in the examples it confusingly uses this baseUrl
in some cases as both the root to load the FilePicker UI, and also in some cases as the "resource" used when specifying scopes when requesting an auth token. The reason this is unhelpful as it is specified in the examples is that, in most cases, we won't actually know the "baseUrl" until the user authenticates using OAuth and we can query to get information about the user's tenant.
All that said, here are the major things I had to do differently between consumer accounts and multitenant accounts to get everything working.
PublicClientApplication
required for OAuth (I used the @azure/msal-browser
NPM module, e.g. https://github.com/OneDrive/samples/blob/master/samples/file-picking/typescript-react/src/auth.ts#L7 ), for consumer accounts the authority
config property should be "https://login.microsoftonline.com/consumers", and for business ("organizational") accounts it should be "https://login.microsoftonline.com/common".getToken
code from the examples, it's important to note that this code is used BOTH when originally showing the OAuth popup (or full screen redirect) AND when responding to authenticate
messages from the Picker control. That code calls both acquireTokenSilent
and loginPopup
(e.g. see https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/acquire-token.md and https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md). For consumer accounts, the { scopes: [..] }
object passed to those methods should just be, statically, { scopes: ["OneDrive.ReadOnly"] }
. For business accounts, the first time I logged in the user I just used the static scope of { scopes: [".default"] }
. However, when responding to authenticate messages from the Picker control (e.g. what gets called here, https://github.com/OneDrive/samples/blob/master/samples/file-picking/javascript-basic/index.html#L122) you need to prepend the scope with the name of the "resource" passed, so the code here becomes essentially { scopes: [`${message.data.data.resource}/.default`] }
.const loadPickerUrl = `${pickerBaseUrl}?${new URLSearchParams({ filePicker: JSON.stringify(pickerConfig) })}`;
.In the consumer case:
const pickerBaseUrl = "https://onedrive.live.com/picker"; // this is static, doesn't change for the consumer case
const pickerConfig = {
sdk: "8.0",
entry: {
sharePoint: {
byPath: { list: "https://onedrive.live.com" }, // this is also static and doesn't change
},
},
authentication: {},
messaging: {
origin: window.location.origin,
channelId = "...", // set the channel ID to some unique value, e.g. a uuid
},
// for my purposes I wanted to allow multiple file selection
typesAndSources: { mode: "files" },
selection: { mode: "multiple" },
search: { enabled: true },
};
That is, for the consumer case, the pickerBaseUrl
and the pickerConfig.entry.sharePoint.byPath.list
values are static.
In the multitenant business account case, things become more complicated because after the user first logs in through OAuth, we need to query the API to get the values. There are no examples of this in the doc, and only one small FAQ at https://github.com/OneDrive/samples/tree/master/samples/file-picking#faq that was helpful:
const token = ...; // get a token e.g. as described above and in the examples
// need to determine the user's main OneDrive URL:
const usersMainDriveResponse = await fetch(
"https://graph.microsoft.com/v1.0/me/drive",
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
},
);
const usersMainDriveWebUrlString = (await usersMainDriveResponse.json())
.webUrl;
// now we have the info for this user's pickerBaseUrl and pickerConfig.entry.sharePoint.byPath.list values
const pickerBaseUrl = `${new URL(usersMainDriveWebUrlString).origin}/_layouts/15/FilePicker.aspx`;
const pickerConfig = {
sdk: "8.0",
entry: {
sharePoint: {
byPath: { list: usersMainDriveWebUrlString },
},
},
authentication: {},
messaging: {
origin: window.location.origin,
channelId = "...", // set the channel ID to some unique value, e.g. a uuid
},
// for my purposes I wanted to allow multiple file selection
typesAndSources: { mode: "files" },
selection: { mode: "multiple" },
search: { enabled: true },
};
Again, I hope the above is useful for someone else. To Microsoft folks, I would strongly suggest adding an explicit example for the multitenant case. Even better, I think it would be much better to wrap the Picker control as a JS object - there is much too much manual setup (e.g. setting up the communication port between windows) IMO for what should be a client API.
I'll doc the steps for the download-in-javascript case in a subsequent comment.
In order to download picked files from javascript (that is, when responding to the "pick" message, and note the token
variable used below is one that was cached when responding to the picker's authenticate
message):
for (const pickedItem of message.data.data.items) {
// to first get more detailed info about the picked item, including the mime type of the file
const fileInfoUrl = `${pickedItem["@sharePoint.endpoint"]}/drives/${pickedItem.parentReference.driveId}/items/${pickedItem.id}`;
const infoResponse = await fetch(fileInfoUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const fileInfo = await infoResponse.json();
// now you can download the file contents at the /content url
const downloadUrl = `${fileInfoUrl}/content`;
const downloadResponse = await fetch(downloadUrl, {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
const bodyContents = await downloadResponse.arrayBuffer();
const downloadedFile = new File([bodyContents], fileInfo.name, { type: fileInfo.file.mimeType });
// TODO - do something with the downloadedFile
}
@adevine The documentation in this issue has saved me MANY hours. Honestly thank you so much.
and I completely second this statement:
To Microsoft folks, I would strongly suggest adding an explicit example for the multitenant case. Even better, I think it would be much better to wrap the Picker control as a JS object - there is much too much manual setup (e.g. setting up the communication port between windows) IMO for what should be a client API.
@adevine Thanks for much for this awesome explanation! I'm still running into some issues though, would you be able to give a full example?
Category
There are a couple of issues around multitenant support for the FilePicker control (#1636 and #1684) that are unfortunately closed, but with no actual good solutions. This seems to be a common problem because there are similar unanswered questions online at https://learn.microsoft.com/en-us/answers/questions/1165692/multi-tenant-setup-for-sharepoint-v8-for-filepicke and https://techcommunity.microsoft.com/t5/onedrive-developer/base-url-used-for-multi-tenants/m-p/3792211.
The overview of what I'm trying to do is the same as listed in those linked issues and comments: I would like to provide a control where people can pull down files from their OneDrive accounts (both personal and business) and then upload them to my application. As a working example of this, I essentially want to do exactly what ChatGPT recently added with their "Connect to Microsoft OneDrive" file upload feature.
I understand how to set up multitenant in the Azure portal in the AAD registration ("Supported account types" is "Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)"). The problem is that the FilePicker requires a
baseUrl
before I know what the user's tenant is going to be - this feels like a catch 22 to me. Here is the relevant section from the doc in the Initiate the Picker section:What I don't understand at all is what I should set that
baseUrl
value to if I wish to allow any user to authenticate with their OneDrive/Sharepoint account. I think it would really, really help cut down on confusion if the doc just outlined the following cases:Thanks for your assistance.