auth0-spa-js icon indicating copy to clipboard operation
auth0-spa-js copied to clipboard

Allow other API to open popups besides window.open

Open kcarnold opened this issue 1 year ago • 6 comments

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:

  1. The popup must start in the same domain (it can redirect out of it).
  2. 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)
  3. 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

kcarnold avatar Nov 13 '24 13:11 kcarnold

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!

kcarnold avatar Nov 15 '24 20:11 kcarnold

We've realized that we sometimes want to call getTokenWithPopup, and face this same problem.

kcarnold avatar May 23 '25 16:05 kcarnold

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:

  • loginWithRedirect takes an openUrl that allows you to take full control.
  • loginWithPopup allows 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.

frederikprijck avatar May 26 '25 06:05 frederikprijck

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.

kcarnold avatar May 27 '25 20:05 kcarnold

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.

frederikprijck avatar May 28 '25 11:05 frederikprijck

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.

kcarnold avatar May 28 '25 13:05 kcarnold

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!

gyaneshgouraw-okta avatar Sep 28 '25 15:09 gyaneshgouraw-okta