Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OneDrive FilePicker Multitenant Instructions are still unclear #1858

Open
2 of 3 tasks
adevine opened this issue Jun 29, 2024 · 5 comments
Open
2 of 3 tasks

OneDrive FilePicker Multitenant Instructions are still unclear #1858

adevine opened this issue Jun 29, 2024 · 5 comments

Comments

@adevine
Copy link

adevine commented Jun 29, 2024

Category

  • Question
  • Documentation issue
  • Bug

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:

To initate the picker you need to create a "window" which can either be an iframe or a popup. Once you have a window you should construct a form and POST the form to the URL {baseUrl}/_layouts/15/FilePicker.aspx with the query string parameters defined.

The {baseUrl} value above is either the SharePoint web url of the target web, or the user's onedrive. Some examples are: "https://tenant.sharepoint.com/sites/dev" or "https://tenant-my.sharepoint.com".

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:

  1. baseUrl assuming it's a single tenant app, i.e. users must belong to the same tenant as where the application was registered.
  2. baseUrl in the multi tenant app case.
  3. baseUrl for personal OneDrive accounts (I haven't tested this yet but the doc already appears to state this as https://onedrive.live.com/picker).

Thanks for your assistance.

@adevine
Copy link
Author

adevine commented Jul 18, 2024

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:

  1. How to get the OneDrive Picker working for a range of accounts (i.e. consumer accounts, business accounts on both the same tenant and multi tenant, etc.).
  2. How to download files once selected in the Picker in javascript (i.e. where CORS access is allowed), and not just through a download link.

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.

  1. First, as noted in the doc here, I created an AAD App Registration in the Azure console at https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade . I set it up for multitenant access by setting the option for "Supported Account Types" to "Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)".
  2. There are a couple of main config settings that differ between consumer accounts and business accounts. First, when creating the 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".
  3. In the 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`] }.
  4. The hardest thing for me to find was how to set up the URL to call to load the picker for both the consumer case and the multitenant case. In both cases the URL is formed as 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.

@adevine
Copy link
Author

adevine commented Jul 18, 2024

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
}

@jeropaul
Copy link

jeropaul commented Sep 18, 2024

@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.

@emelabnext
Copy link

emelabnext commented Oct 29, 2024

@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?

@yogesh200002
Copy link

I also encountered murky documentation from MS. This is how I solved the picker issues when integrating with personal and multi-tenant org users. It is for documentation purposes. Thanks to @adevine for pointing me in the right direction. I solved this issue by seeing the implementation done by ChatGPT.

Steps I did

1. Registering the App

  1. First and foremost, we have to create a client ID with a redirect URL for our app to use the OAuth permissions and access resources accordingly. For that, we have to go to Azure Portal > Microsoft Entra ID > App Registrations > Register App.
  2. In that it will ask which users we are targetting for. Since we are targeting both personal and organization tenant users we have to pick Accounts in any Organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (eg Skype. Xbox) this option.
  3. For the platform, we can now choose SPA since on the time of writing the application is in SPA mode.
  4. For redirect URL, for testing we can use localhost:3000 for now. Since this is a SPA app, the redirect is not gonna do anything. This will be explained later.
  5. After registering the App, go to the overview. You will see a Application (client) ID This is the thing we need for accessing the MSAL (Microsoft Authentication Library) and for the Onedrive file picker.
  6. Under API permissions Add Files.Read.All, Files.Read, and User.Read for Graph delegated permissions. Files is for Onedrive and SharePoint sites respectfully and User is for getting user profile.

2. Integrating the picker into our application

  1. Now install these two libraries @azure/msal-browser and @azure/msal-react Both libraries are official libraries from MS to use their OAuth and grant access to the resources. Since I used React for our application, If you are not using react you can just use the @azure/msal-browser and use its public instance.
  2. Create a Provider and add it on top of your App.
	import {        
	    BrowserCacheLocation,
	    PublicClientApplication,
    } from  "@azure/msal-browser";
    import { MsalProvider } from  "@azure/msal-react";
              
    /**
    * @type  {import("@azure/msal-browser").Configuration} MSAL configuration
    * Refer https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md for more info
    */
        
    const  configuration = {
        auth: {
            clientId:  <insert client id>,
        },
        cache: {
            cacheLocation:  BrowserCacheLocation.MemoryStorage,
            storeAuthStateInCookie:  true,
            secureCookies:  import.meta.env.PROD,
        },
        system: {
            windowHashTimeout:  9000, // Applies just to popup calls - In milliseconds
            iframeHashTimeout:  9000, // Applies just to silent calls - In milliseconds
            loadFrameTimeout:  9000, // Applies to both silent and popup calls - In milliseconds
        },
    };
        
    const  pca  =  new  PublicClientApplication(configuration);    
    
    export  default  function  MSALProvider({ children }) {
        return  <MsalProvider  instance={pca}>{children}</MsalProvider>;   
    }
  1. Now we can use the auth hooks from @azure/msal-react. For this create to button to ask for login if the accounts do not exist.
const { instance, accounts } = useMsal(); // This is for getting the Instance of MSAL and accounts that are signed in.

// For personal accounts this will always be the tid. Refer https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
function checkPersonalAccount({ idTokenClaims: { tid } }) {
	return tid === "9188040d-6c67-4c5b-b112-36a304b66dad";
}

function checkOrgAccount({ idTokenClaims: { tid } }) {
	return tid !== "9188040d-6c67-4c5b-b112-36a304b66dad";
}

const personalAccount = accounts.find(checkPersonalAccount);
const orgAccount = accounts.find(checkOrgAccount);

// Now we can ask for login if the account doesn't exist
// This should be handled in a button event :). 
if(personalAccount == null){
    const url = window.location.origin;
	instance.loginPopup({
		redirectUri: `${url}`,
		scopes: oneDriveOrgScopes, // This scopes and authority will be explained below.
		authority: msalOrgAuthority,
	});
}else{
    // Later points
}
  1. When asking for login we have to use scopes which we defined earlier as API permissions in the previous steps. Based on these login scopes only we can able to access their resources. If the resources you ask for don't match the API permissions this will throw an error and the user won't be able to log in.
    For Personal Users, You have to use authority as https://login.microsoftonline.com/consumers and scopes as [OneDrive.ReadWrite]. For Org users, you can be implict and ask the same permissions that you defined, for them authority is https://login.microsoftonline.com/organizations and scopes is ["Files.Read.All", "Files.Read", "User.Read"].

  2. Now after the user logs in, MSAL will store a refresh token only, for getting the resources we have to get an access token and pass it later on. Use the below function to get the access token.

/**
 * Combines an arbitrary set of paths ensuring and normalising the slashes
 * This is used for getting scopes based on the resource type in Onedrive Picker
 * @param paths 0 to n path parts to combine
 */
export function combine(...paths) {
	return paths
		.map((path) => path.replace(/^[\\|/]/, "").replace(/[\\|/]$/, ""))
		.join("/")
		.replace(/\\/g, "/");
}


/**
 *
 * @param {Object} Properties
 * @param {import("@azure/msal-browser").IPublicClientApplication} Properties.instance MSAL Instance to use
 * @param {string[]} Properties.scopes Scopes to ask for in the token. This is overridden when type and resource are passed
 * @param {import("@azure/msal-browser").AccountInfo} Properties.currentUser User Account to get the access token for
 * @param {string} Properties.Authority URL to use for OAuth
 * @param {string | undefined} Properties.type Which Type of Resource to fetch from, this is only used in the onedrive file picker event listener. This will be used to get access tokens according to the resource
 * @param {string | undefined} Properties.resource Which Resource to scope for, this is only used in the one drive file picker event listener.
 * @returns {Promise<string>} Access Token for the particular scope
 */
export async function getOneDriveAccessToken({
	currentUser,
	instance,
	scopes,
	authority,
	type,
	resource,
}) {
	let accessToken = "";
	let currentScopes = scopes;

	switch (type) {
		case "SharePoint":
		case "SharePoint_SelfIssued":
			currentScopes = [`${combine(resource, ".default")}`];
			break;
		default:
			break;
	}

	try {
		// See if we have already the id token saved
		const resp = await instance.acquireTokenSilent({
			scopes: currentScopes,
			authority,
			account: currentUser,
		});
		accessToken = resp.accessToken;
	} catch (e) {
		if (e instanceof InteractionRequiredAuthError) {
			return instance.acquireTokenPopup({
				scopes: currentScopes,
				authority,
				account: currentUser,
			});
		} else {
			throw e;
		}
	}
	return accessToken;
}
  1. Now we have all the things required to access the resources of the user. But we missed out on the Onedrive file picker. So we are going to define the configuration for our picker.
/**
 * Entry is left blank because it is based on whether the user is org-based or personal.
 * Refer https://learn.microsoft.com/en-us/onedrive/developer/controls/file-pickers/v8-schema
 */
export const oneDrivePickerOptions = {
	sdk: "8.0",
	entry: {},
	authentication: {},
	messaging: {
		origin: window.location.origin,
		channelId: crypto.randomUUID(), // This is like a state that only the host and this picker can able to used, other people can't intercept it.
	},
	typesAndSources: {
		mode: "files",
		filter: ["photo", "document"],
		pivots: {
			oneDrive: true,
			recent: true,
			myOrganization: true,
		},
	},
	search: {
		enabled: true,
	},
	selection: {
		mode: "multiple",
		enablePersistence: true,
	},
};
  1. Now we can start using the picker, After logging in we can use the else condition from the 3. point now. This is where things get very different based on personal and org users
if(personalAccount != null){
    //
}else{
    const oneDriveWindow = window.open(
        "",
        "Picker",
        "width=1080,height=680"
    );
    const accessToken = await getOneDriveAccessToken({
        instance,
        currentUser: personalAccount,
        authority: msalConsumerAuthority,
        scopes: oneDriveConsumerScopes,
    });
    const queryString = new URLSearchParams({
        filePicker: JSON.stringify({
            ...oneDrivePickerOptions,
            entry: { // See the entry difference for org
                oneDrive: {
                    files: {},
                },
            },
        }),
        locale: "en-us",
    });

    // We can use this URL for getting personal account files
    const oneDriveConsumerBaseUrl = "https://onedrive.live.com/picker";
    const url = `${oneDriveConsumerBaseUrl}?${queryString}`;
    const form = oneDriveWindow.document.createElement("form");
    //Set the action of the form to the URL defined above
    // This will include the query string options for the picker.
    form.setAttribute("action", url);

    // must be a post request
    form.setAttribute("method", "POST");
    oneDriveWindow.document.body.append(form);

    // Create a hidden input element to send the OAuth token to the Picker.
    // This is optional when using a popup window but required when using an iframe.
    const tokenInput =
        oneDriveWindow.document.createElement("input");
    tokenInput.setAttribute("type", "hidden");
    tokenInput.setAttribute("name", "access_token");
    tokenInput.setAttribute("value", accessToken);
    form.appendChild(tokenInput);

    //Submit the form, this will load the picker page
    form.submit();

    window.addEventListener("message", (event) => {
        if (event.source && event.source === oneDriveWindow) {
            const message = event.data;
            if (
                message.type === "initialize" &&
                message.channelId ===
                    oneDrivePickerOptions.messaging.channelId
            ) {
                const port = event.ports[0];
                port.addEventListener(
                    "message",
                    onedrivePortEventListener({ // This will be explained below.
                        port,
                        oneDriveWindow,
                        type: "personal",
                    })
                );
                port.start();
                port.postMessage({
                    type: "activate",
                });
            }
        }
    });

}

For Org

if (orgAccount != null) {
    const oneDriveWindow = window.open(
    "",
    "Picker",
    "width=1080,height=680"
    );
    const accessToken = await getOneDriveAccessToken({
        instance,
        currentUser: orgAccount,
        scopes: oneDriveOrgScopes,
        authority: msalOrgAuthority,
    });

    // This is to get the org drive URL, Since this is not a personal account
    const {
        data: { webUrl },
    } = await axios.get(
        "https://graph.microsoft.com/v1.0/me/drive",
        {
            headers: {
                Authorization: `Bearer ${accessToken}`,
                "Content-Type": "application/json",
            },
            withCredentials: false,
        }
    );

    const queryString = new URLSearchParams({
        filePicker: JSON.stringify({
            ...oneDrivePickerOptions,
            entry: { // See the entry point differnce for org
                sharePoint: {
                    byPath: { list: webUrl },
                },
            },
            site: {},
        }),
        locale: "en-us",
    });
    const url = `${new URL(webUrl).origin}/_layouts/15/FilePicker.aspx?${queryString}`;

    // Same form methods as above
 
    window.addEventListener("message", (event) => {
        if (event.source && event.source === oneDriveWindow) {
            const message = event.data;
            if (
                message.type === "initialize" &&
                message.channelId ===
                    oneDrivePickerOptions.messaging.channelId
            ) {
                const port = event.ports[0];
                port.addEventListener(
                    "message",
                    onedrivePortEventListener({
                        port,
                        oneDriveWindow,
                        type: "org",
                    })
                );
                port.start();
                port.postMessage({
                    type: "activate",
                });
            }
        }
    });
  1. For the onedrivePortEventListener, use the below function.
    /**
	 * Listens to Events sent by the one drive window
	 * @param {{port:WindowProxy,oneDriveWindow:Window, type:"personal"|"org"}} param0
	 */
	const onedrivePortEventListener =
		({ port, oneDriveWindow, type }) =>
		async (message) => {
			switch (message.data.type) {
				case "notification":
					break;
				case "command": {
					port.postMessage({
						type: "acknowledge",
						id: message.data.id,
					});
					const {
						command,
						items, // This is the files picked from the picker
						type: commandType,
						resource,
					} = message.data.data;
                    // This is the place, Where the documentation missed out on a key detail but it will be used in their sample codes. They don't explain why it is needed.
					const tokenOptions =
						type === "personal"
							? {
									scopes: oneDriveConsumerScopes,
									authority: msalConsumerAuthority,
									currentUser: personalAccount,
									instance,
								}
							: {
									scopes: oneDriveOrgScopes,
									authority: msalOrgAuthority,
									currentUser: orgAccount,
									instance,
									type: commandType, // In the getOneDriveAccessToken, you would have seen one switch statement based on type. For tenant users we can't use the same resource rather the picker emits this resource and their access type for that we have to get an access token.
									resource,
								};
					switch (command) {
						case "authenticate": {
                            // Based on the token options above, we can send the token to the picker
							const token =
								await getOneDriveAccessToken(tokenOptions);
							if (token != null) {
								port.postMessage({
									type: "result",
									id: message.data.id,
									data: {
										result: "token",
										token,
									},
								});
							} else {
								enqueueSnackbar(
									`Could not get auth token for command: ${command}`,
									{
										variant: "error",
									}
								);
							}
							break;
						}

						case "close":
							oneDriveWindow.close();
							break;

						case "pick": {
                            // You can use the items from message.data.data and get the files picked by the users.
							port.postMessage({
								type: "result",
								id: message.data.id,
								data: {
									result: "success",
								},
							});
							oneDriveWindow.close();
							break;
						}

						default:
							enqueueSnackbar(`Unsupported command: ${command}`, {
								variant: "warning",
							});
							port.postMessage({
								result: "error",
								error: {
									code: "unsupportedCommand",
									message: command,
								},
								isExpected: true,
							});
							break;
					}
					break;
				}
				default:
					break;
			}
		};
  1. For downloading the files you can use the below function. This also differs based on personal and org accounts.
/**
 * Get Onedrive Download URL
 * @param {{parentReference:{driveId:string},id:string,"@sharePoint.endpoint"?:string}} Object you get from message.data.data.items that is emitted by the picker
 * @param {string} accessToken
 * @param {"org" | "personal"} type
 * @returns {Promise<string>} Download URL
 */
export async function getOnedriveDownloadUrlFromPickerObject(
	pickerObj,
	accessToken,
	type
) {
	const {
		parentReference: { driveId },
		id,
	} = pickerObj;
	if (type === "org") {
		const { data } = await axios.get(
			`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${id}`,
			{
				withCredentials: false,
				headers: {
					Authorization: "Bearer " + accessToken,
				},
			}
		);
		return data["@microsoft.graph.downloadUrl"];
	} else {
		const { data } = await axios.get(
			pickerObj["@sharePoint.endpoint"] +
				"/drives/" +
				driveId +
				"/items/" +
				id,
			{
				withCredentials: false,
				headers: {
					Authorization: "Bearer " + accessToken,
				},
			}
		);
		return data["@content.downloadUrl"];
	}
}

Final Notes

Based on this you can have two buttons to use for picker so that the user can either use a personal account or an org account. Instead of signing in with only a single account. If you don't want to use two account logins, you can use the https://login.microsoftonline.com/common so that the login can support both personal and org accounts in a single login popup. But after logging in you have to check whether it is a personal or org account and handle the scopes, picker config and download urls accordingly. if you are not using react you can use the functions available in the instance which is provided by the @azure/msal-browser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants