expo: [expo-web-browser] Popup being blocked by Desktop Safari when using WebBrowser.openAuthSessionSync

πŸ› Bug Report

Summary of Issue

On Expo Web, popups by WebBrowser are being blocked by Desktop Safari. This is likely because of some async calls which prevent Safari from recognizing that the popup was requested via a user interaction.

Environment - output of expo diagnostics & the platform(s) you’re targeting

  Expo CLI 3.27.4 environment info:
    System:
      OS: macOS Mojave 10.14.6
      Shell: 3.2.57 - /bin/bash
    Binaries:
      Node: 12.14.0 - ~/.nvm/versions/v12.14.0/bin/node
      Yarn: 1.22.4 - /usr/local/bin/yarn
      npm: 6.13.4 - ~/.nvm/versions/v12.14.0/bin/npm
      Watchman: 4.7.0 - /usr/local/bin/watchman
    Managers:
      CocoaPods: 1.5.2 - /usr/local/bin/pod
    SDKs:
      iOS SDK:
        Platforms: iOS 13.2, DriverKit 19.0, macOS 10.15, tvOS 13.2, watchOS 6.1
    IDEs:
      Android Studio: 3.6 AI-192.7142.36.36.6241897
      Xcode: 11.3/11C29 - /usr/bin/xcodebuild
    npmPackages:
      @expo/webpack-config: ^0.12.29 => 0.12.29 
      expo: ^38.0.0 => 38.0.10 
      react: 16.11.0 => 16.11.0 
      react-dom: 16.11.0 => 16.11.0 
      react-native: https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz => 0.62.2 
      react-native-web: ~0.11.7 => 0.11.7 
    npmGlobalPackages:
      expo-cli: 3.27.4
    Expo Workflow: managed

Reproducible Demo

<TouchableWithoutFeedback onPress={() => {
  WebBrowser.openAuthSessionAsync('https://www.hackaday.com', 'https://www.itsame.com');
}}>
  <View style={{width: 50, height: 50, backgroundColor: 'red'}} />
</TouchableWithoutFeedback>

Steps to Reproduce

Use the snippet above and press the Touchable.

Expected Behavior vs Actual Behavior

Window should open in Desktop Safari just like it does in Chrome. Instead Safari blocks the popup and asks the user to manually confirm.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 15 (9 by maintainers)

Commits related to this issue

Most upvoted comments

I’ve found where is the problem: https://github.com/expo/expo/blob/a228d5016823b51289c51046b55985bd3f6822f2/packages/expo-web-browser/src/ExpoWebBrowser.web.ts#L124

Function getStateFromUrlOrGenerateAsync is creating new crypto state if state param is not passed by url param. This is causing blocking popup on desktop Safari. The case why is working with AuthSession.useAuthRequest(…) is because it creates internally crypto state and pass it as a url param.

As a fix I would propose moving code responsible for getting/creating crypto state right after window.open call. I’ve test it locally and it works.

For example:

    if (popupWindow == null || popupWindow?.closed) {
      const features = getPopupFeaturesString(openOptions?.windowFeatures);
      popupWindow = window.open(url, openOptions?.windowName, features);

      if (popupWindow) {
        try {
          popupWindow.focus();
        } catch (e) {}
      } else {
        throw new CodedError(
          'ERR_WEB_BROWSER_BLOCKED',
          'Popup window was blocked by the browser or failed to open. This can happen in mobile browsers when the window.open() method was invoked too long after a user input was fired.'
        );
      }
    }

    const state = await getStateFromUrlOrGenerateAsync(url);

    // Save handle for session
    window.localStorage.setItem(getHandle(), state);
    // Save redirect Url for further verification
    window.localStorage.setItem(getRedirectUrlHandle(state), redirectUrl);

What do you think ?

@brentvatne Basically in order to not being blocked by the browser, we need to use the new secure API called Credential Management: https://developers.google.com/web/fundamentals/security/credential-management/

There is a web-component already implemented this and has a nice demo: https://github.com/pwa-builder/pwa-auth

Is it okay that I start the implementation of this fix?

The fix just landed in expo-web-browser@10.2.1, in case anybody wants to remove their manual workarounds. Thanks again to @scyrych for suggesting the fix in the first place πŸ˜ƒ

@jarvisluong - yup! that would be great.

After digging for a while, I can share some more findings:

  1. Exactly the same issue in Firefox both desktop and mobile.
  2. As @zoskia mentioned, AuthSession.startAsync() suffers from the same issue
  3. WebBrowser.openBrowserAsync() works perfectly fine
  4. MOST INTERESTING: This piece of code also works fine, even though, promptAsync() internally makes use of the very same WebBrowser.openAuthSessionAsync():
const [request, response, promptAsync] = AuthSession.useAuthRequest(...);
...
onPress -> promptAsync()