Allow other API to open popups besides window.open
Checklist
- [X] I have looked into the Readme, Examples, and FAQ and have not found a suitable solution or answer.
- [X] I have looked into the documentation and API documentation, and have not found a suitable solution or answer.
- [X] I have searched the issues and have not found a suitable solution or answer.
- [X] I have searched the Auth0 Community forums and have not found a suitable solution or answer.
- [X] I agree to the terms within the Auth0 Code of Conduct.
Describe the problem you'd like to have solved
Microsoft Office add-ins require that any popups be opened using a special API instead of window.open. A few unusual constraints of that API are:
- The popup must start in the same domain (it can redirect out of it).
- There isn't a reliable, cross-platform way to send information to the dialog after it's opened (i.e., you should package everything you need to send into the query string)
- Sending data back from the popup also requires a custom API.
Describe the ideal solution
I've done some gymnastics (see below) to make this API work with loginWithPopup, but it would be much easier if the user of the library could provide a custom dialog API. For example, something like:
loginWithPopup({}, {
popupController: async (popupURL) => {
const bounceURL = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + '/popup.html?redirect=' + encodeURIComponent(popupURL);
const dialog = await asyncify(Office.context.ui.displayDialogAsync)(bounceURL, OTHER_OPTIONS);
const tokenReply = await waitForMessageReceived(dialog);
return tokenReply;
}
});
where I've imagined asyncified versions of the Office dialog code.
Alternatives and current workarounds
My workaround code currently looks like:
let dialog: Office.Dialog;
const processMessage = async (
args:
| { message: string; origin: string | undefined }
| { error: number }
) => {
if ('error' in args) {
// eslint-disable-next-line no-console
console.error('Error:', args.error);
return;
}
// eslint-disable-next-line prefer-const
let messageFromDialog = JSON.parse(args.message);
dialog.close();
if (messageFromDialog.status === 'success') {
// The dialog reported a successful login.
// eslint-disable-next-line prefer-const
let token = messageFromDialog.auth0Token;
// eslint-disable-next-line no-console
console.log('Login successful.', token);
// Mock the window message event that auth0-spa-js expects
// see https://github.com/auth0/auth0-spa-js/blob/f2e566849efa398ca599daf9ebdfbbd62fcb1894/__tests__/utils.test.ts#L234
let messageEvent = new MessageEvent('message', {
data: {
type: 'authorization_response',
response: {id_token: token}
}
})
window.dispatchEvent(messageEvent);
}
else {
// eslint-disable-next-line no-console
console.error('Login failed.', messageFromDialog);
}
};
// Make the href of the popup be a setter so that we can actually launch the dialog with the correct url to begin with
const mockPopup = {
location: {
set href(url: string) {
console.log("Setting location.href to", url);
// Set up an Office dialog to do the login flow
// height and width are percentages of the size of the screen.
// How MS use it: https://github.com/OfficeDev/Office-Add-in-samples/blob/main/Samples/auth/Office-Add-in-Microsoft-Graph-React/utilities/office-apis-helpers.ts#L38
// Bounce off /popup.html?redirect=... to get the token
let redirect = encodeURIComponent(url);
let bounceURL = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + '/popup.html?redirect=' + redirect;
console.log("Bouncing to", bounceURL);
Office.context.ui.displayDialogAsync(
bounceURL,
{ height: 45, width: 55 },
function (result) {
dialog = result.value;
dialog.addEventHandler(
Office.EventType.DialogMessageReceived,
processMessage
);
}
);
}
},
closed: false,
close: () => {mockPopup.closed = true},
};
...
await loginWithPopup({}, {popup: mockPopup});
Additional context
No response
Nevermind, the popup approach doesn't actually work here at all because auth0 tries to use a webmessage response, and Office doesn't allow cross-origin messages from dialogs without its special API too.
But I realized that loginWithRedirect is a more robust flow. Our WIP approach:
if (!isAuthenticated) {
let dialog: Office.Dialog;
// Strategy: the popup will pass its redirect-callback data here, so we can pass it on to handleRedirectCallback
const processMessage = async (
args:
| { message: string; origin: string | undefined }
| { error: number }
) => {
if ('error' in args) {
console.error('Error:', args.error);
return;
}
let messageFromDialog = JSON.parse(args.message);
dialog.close();
if (messageFromDialog.status === 'success') {
// The dialog reported a successful login.
handleRedirectCallback(messageFromDialog.urlWithAuthInfo);
}
else {
console.error('Login failed.', messageFromDialog);
}
};
// Actually make a popup using MS dialog API
// hook the message event from the popup to set close false and get the token
return (
<div>
Login here:
<button onClick= { async () => {
// Use this dialog for the Auth0 client library.
await loginWithRedirect({
openUrl: async (url: string) => {
const redirect = encodeURIComponent(url);
const bounceURL = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + '/popup.html?redirect=' + redirect;
// height and width are percentages of the size of the screen.
// How MS use it: https://github.com/OfficeDev/Office-Add-in-samples/blob/main/Samples/auth/Office-Add-in-Microsoft-Graph-React/utilities/office-apis-helpers.ts#L38
Office.context.ui.displayDialogAsync(
bounceURL,
{ height: 45, width: 55 },
function (result) {
dialog = result.value;
dialog.addEventHandler(
Office.EventType.DialogMessageReceived,
processMessage
);
}
);
}
});
}}
>Log in
</button>
</div>
);
}
Handling log-out is a different story... we haven't figured that out yet!
We've realized that we sometimes want to call getTokenWithPopup, and face this same problem.
Trying to understand the specific ask here a bit better. Avoiding using the browser's API's to open the URL to Auth0 can be achieved using openUrl on loginWithRedirect. Unless I misunderstand, this is what you are looking for.
I do not think there is much of a change we can implement if you know that:
loginWithRedirecttakes anopenUrlthat allows you to take full control.loginWithPopupallows for you to pass in a popup. Sure in the current code in this issue it looks a bit weird, but in essence you want to use an adapter that adapts your dialog implementation to the browser one, as that would help with keeping the implementation a bit cleaner.
I think the loginWithPopup code flow simply didn't work because the popup tried to postMessage to its parent, and Office dialogs need to use Office.messageParent. We worked around that using loginWithRedirect (as you described) for login, but there's not a corresponding getTokenWithRedirect so we can't use that code path for redirect token flows.
We're thinking about simply dropping auth0 and rolling our own oauth2; we've spent too much effort on all the papercuts here. (Maybe a custom domain could fix it, but I'm not sure how because it would still technically be cross-origin: app.example.com vs login.example.com.)
I'm not going to submit a PR myself here, but I got a reasonable response when I messaged GitHub Copilot with this request in Auth0Client.ts:
the loginWithPopup and getTokenWithPopup methods are tied to specifics of the browser popup API. For example, Office add-ins need to use displayDialogAsync and messageParent. Could we decouple these implementations from the dialog API while simultaneously simplifying the control flow?
But note that this still doesn't address the problem of using the wrong API to message parent; at least the response_type parameter would need to be customizable I guess.
but there's not a corresponding getTokenWithRedirect so we can't use that code path for redirect token flows.
There is no such thing to just retrieve a token using a redirect when relying on the Authorization Code Flow. If you want to redirect, you can use loginWithRedirect.
Maybe a custom domain could fix it, but I'm not sure how because it would still technically be cross-origin: app.example.com vs login.example.com.
Not sure what you want to solve with custom domains, but custom domains does help solve 3rd party cookie issues as it only accounts for the top level domain.
That said, this SDK is built for SPA applications, not Office Addins. Happy to see how we can help you integrate it, but it's expected not to be just plug and play.
To be clear: we're using loginWithRedirect as a hacky workaround to actually open a popup using the correct dialog API (see #1274 which has your suggested workaround and a link to our implementation of it). My point with the lack of getTokenWithRedirect is that we can't apply that same workaround to get a token if getTokenSilently fails. (And I'd expect that the iframe fallback would fail because of the cross-origin issue, but that's difficult to test so I haven't.)
I'm not asking for this to be plug-and-play, but if the code were more modular we could build our solution using its pieces.
Hi @kcarnold,
Thanks for the detailed exploration of this use case. I will be converting this to a GitHub Discussion.
- This is a highly specialized use case for Microsoft Office add-ins that affects a niche subset of users
- The request involves architectural changes that would benefit from community exploration and input
- Your detailed workarounds and insights would be more discoverable to other developers facing similar Office add-in integration challenges
- Discussions are better for complex issues that need collaborative problem-solving.
- Your Auth0 integration work is valuable, and a discussion will help others contribute and build on it.
Thanks again for your contributions!