Open
Description
I'm attempting to implement changes as suggested by this post to use the One Drive JS picker with SDK v8 but having one button/link instead of two. I've run into multiple errors, and currently seeing this error:
invalid_request: 900023 - [2025-04-23 20:55:37Z]: AADSTS900023: Specified tenant identifier 'consumer' is neither a valid DNS name, nor a valid external domain. Trace ID: b139cb9f-e16b-4a40-b3ec-1c4e1c2c2100
Honestly this feels like a big game of whack-a-mole... -_-. Has anyone been successful in loading the One drive picker for both consumer and organization accounts?
Below is the script used to handle clicks on the picker button (rendered from a Laravel Blade PHP template - so blocks like {!! !!} will be replaced with a value):
const baseUrl = "https://onedrive.live.com/picker";
const msalConsumerAuthority = 'https://login.microsoftonline.com/consumer';
const msalOrgAuthority = 'https://login.microsoftonline.com/common';
let currentUser;
let type = 'org';
const delegateId = '{!! $delegateId !!}';
const msalParams = {
auth: {
authority: "https://login.microsoftonline.com/common",
clientId: "{!! $CLIENT_ID !!}",
redirectUri: `${window.location.origin}${window.location.pathname}`
},
}
const instance = new msal.PublicClientApplication(msalParams);
const scopes = ['.default']; // 'https://graph.microsoft.com/.default'
const oneDriveConsumerScopes = ['OneDrive.ReadWrite'];
const oneDriveOrgScopes = ['.default']; // 'https://graph.microsoft.com/.default'
// 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";
}
/**
* 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
*/
function combine(...paths) {
return paths
.map((path) => path.replace(/^[\\|/]/, "").replace(/[\\|/]$/, ""))
.join("/")
.replace(/\\/g, "/");
}
/**
*
* @param {Object} Properties
* @param {import("@azure/msal-browser").IPublicClientinstancelication} 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
*/
async function getOneDriveAccessToken({
currentUser,
instance,
scopes,
authority,
type,
resource,
}) {
let accessToken = "";
let currentScopes = scopes;
console.log('getOneDriveAccessToken() - type:', type, 'resource: ', resource, ' scopes: ', scopes, ' authority: ', authority);
switch (type) {
case "SharePoint":
case "SharePoint_SelfIssued":
currentScopes = [`${combine(resource, ".default")}`];
break;
default:
break;
}
const popupOptions = {
scopes: currentScopes,
authority: type === 'org' ? msalOrgAuthority : msalConsumerAuthority
};
if (currentUser) {
popupOptions.account = currentUser
}
try {
console.log('calling instance.acquireTokenSilent() with scopes: ', currentScopes);
// See if we have already the id token saved
const resp = await instance.acquireTokenSilent(popupOptions);
console.log('setting active account after callling acquireTokenSilent(): ', resp.account);
instance.setActiveAccount(resp.account);
accessToken = resp.accessToken;
} catch (e) {
console.log(' getOneDriveAccessToken() - caught e:', e);
if (e.message.includes('invalid_grant') || e.message.includes('no_account_error')) {
console.log('calling acquireTokenPopup() with options: ', popupOptions, 'type: ', type);
return instance.acquireTokenPopup(popupOptions);
} else {
throw e;
}
}
return accessToken;
}
function checkUser(data) {
console.log('checkUser() - data: ', data);
const personalAccount = instance.getAllAccounts().find(checkPersonalAccount);
const orgAccount = instance.getAllAccounts().find(checkOrgAccount);
currentUser = personalAccount || orgAccount;
console.log('checkUser() - personalAccount: ', personalAccount, ' orgAccount: ', orgAccount);
if (personalAccount) {
type = 'personal';
scopes.length = 0;
scopes.push('OneDrive.ReadOnly');
} else {
if (data?.resource) {
scopes.length = 0;
oneDriveOrgScopes.length = 0;
scopes.push(`${combine(data.resource, ".default")}`);
oneDriveOrgScopes.push(`${combine(data.resource, ".default")}`);
console.log('pushed combined scope to scopes', scopes);
} else {
scopes.push('Files.Read.All', 'Files.Read', 'User.Read');
console.log('pushed default files scopes to scopes');
}
}
}
// the options we pass to the picker page through the querystring
const oneDrivePickerOptions = {
sdk: "8.0",
entry: {},
authentication: {},
// prompt: 'consent',
messaging: {
origin: window.location.href,
channelId: "27"
},
selection: {
mode: 'multiple',
enablePersistence: true
},
typesAndSources: {
mode: "files",
pivots: {
oneDrive: true,
recent: true,
},
},
};
const onedrivePortEventListener = ({ port, oneDriveWindow, type }) => async (message) => {
console.log('onedrivePortEventListener() - message:', message, ' type: ', type);
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;
if (command === 'authenticate') {
checkUser(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,
instance,
}
: {
scopes: oneDriveOrgScopes,
authority: msalOrgAuthority,
currentUser,
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);
console.log('authenticated command - token: ', token);
if (token != null) {
port.postMessage({
type: "result",
id: message.data.id,
data: {
result: "token",
token,
},
});
} else {
console.log(`Could not get auth token for command: ${command}`);
}
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:
port.postMessage({
result: "error",
error: {
code: "unsupportedCommand",
message: command,
},
isExpected: true,
});
break;
}
break;
}
default:
break;
}
};
let oneDriveWindow = null;
let port = null;
async function launchPicker(e) {
//const authToken = await getAuthToken();
oneDriveWindow = window.open("", "Picker", "width=800,height=600")
const authToken = await getOneDriveAccessToken({
instance,
currentUser,
type,
authority: msalOrgAuthority, //msalConsumerAuthority,
scopes: ['User.Read'] //oneDriveOrgScopes //oneDriveConsumerScopes,
}); //*/
const queryString = new URLSearchParams({
filePicker: JSON.stringify({
...oneDrivePickerOptions,
entry: { // See the entry difference for org
oneDrive: {
files: {},
},
},
}),
locale: "en-us",
});
const url = `${baseUrl}?${queryString}`;
const form = oneDriveWindow.document.createElement("form");
form.setAttribute("action", url);
form.setAttribute("method", "POST");
oneDriveWindow.document.body.append(form);
const input = oneDriveWindow.document.createElement("input");
input.setAttribute("type", "hidden")
input.setAttribute("name", "access_token");
input.setAttribute("value", authToken);
form.appendChild(input);
console.log('launchPicker() - submitting form');
form.submit();
sendMessageToParentWindow({oneDrivePickerOpened: true});
window.addEventListener("message", (event) => {
console.log('message received: ', event);
if (event.source && event.source === oneDriveWindow) {
const message = event.data;
if (message.type === "initialize" && message.channelId === oneDrivePickerOptions.messaging.channelId) {
port = event.ports[0];
port.addEventListener("message", onedrivePortEventListener({port, oneDriveWindow, type}));
port.start();
port.postMessage({
type: "activate",
});
}
}
});
e.preventDefault();
}
function sendMessageToParentWindow(message) {
if (window.parent != window) {
if (delegateId) {
message.delegateId = delegateId;
}
window.parent.postMessage(message, '*');
}
}
window.addEventListener('DOMContentLoaded', function() {
document.addEventListener('click', launchPicker);
});
Metadata
Metadata
Assignees
Labels
No labels