expo: AuthSession returns dismiss result even before the browser is opened

šŸ› Bug Report

Calling the AuthSession.startAsync for the first time returns the dismiss result immediately after calling. The browser opens correctly in the meantime, but any action done afterwards in the browser is ignored (login correctly, cancel, etc). This means that our users will go to the login screen, login successfully, but the app would understand this session as being dismissed.

In addition, but not connected to this issue directly:

  • On Android, when the user cancels the login (does not open browser, clicks on X in browser, etc) the result sent back to us is also dismiss as well. This means we can not distinguish between cancel and dismiss, which prevents us to create at least a meaningful workaround for this issue.
  • Calling AuthSession.dismiss before starting the session crashes the app (in the Expo client)

Environment

IMPORTANT:

  • only happens on Android
  • only happens in production mode or the built standalone app (not reproducible in the dev mode)
  • only happens on the first call to the AuthSession.startAsync, successive calls bring the expected behaviour
  • confirmed in both simulator and Android device
  Expo CLI 3.11.3 environment info:
    System:
      OS: Linux 5.0 Ubuntu 18.04.3 LTS (Bionic Beaver)
      Shell: 4.4.20 - /bin/bash
    Binaries:
      Node: 10.18.0 - /usr/bin/node
      npm: 6.13.4 - /usr/bin/npm
    npmPackages:
      @types/react: ~16.9.0 => 16.9.16 
      @types/react-native: ~0.60.23 => 0.60.25 
      @types/react-navigation: ^3.0.7 => 3.0.7 
      expo: ^36.0.2 => 36.0.2 
      react: ~16.9.0 => 16.9.0 
      react-native: https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz => 0.61.4 
      react-navigation: ^3.13.0 => 3.13.0 
    npmGlobalPackages:
      expo-cli: 3.11.3

Steps to Reproduce

Simply call AuthSession.startAsync for the first time since the app has been loaded.

Expected Behavior

I would expect that dismissed is not the result, especially that the browser get open and user not knowing what s going proceeds to the login page.

Reproducible Demo

Here is the most simple demo: https://snack.expo.io/rJZ!LryxI

However this needs to be run in the production mode in order to actually get the bug. I am not sure if expo snack supports production mode.

Extra

About this issue

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

Commits related to this issue

Most upvoted comments

I’ve been debugging this and I think I tracked down the issue here.

The problem

In Android and iOS < 11, expo-web-browser used by AuthSession, is using a polyfill version of the NativeAuthSession behaviour available in iOS >= 11.

The way this works is by creating a race condition waiting for one of the following to happen first:

  1. user comes back from the just opened browser to the app, trigger an AppState change (in this case user is assumed to have clicked dismiss or have closed the browser or have come back to the app)
  2. redirect to the returnUrl (default expo app url as intent) as a callback of a successful login action in the browser window (in this case we assume login was successful)

When everything is ok, 2 should happen before 1 and that will dismiss altogether the race condition, resulting in { type: 'success' }.

What happens here is that the first promise in the race condition throws before opening the browser. The reason is that AppState triggers a change event with state active before the browser activity is opened, which means that the change event is triggered without a real AppState change.

This behaviour is actually by design when no other listener to LifecycleEvent has ever been created, as the initial state will be null before AppState grabs it from the bridge (you can verify this by adding another usage of AppState in your app before calling AuthSession features).

The first listener to a LifecycleEvent gets called as soon as it’s added even if state hasn’t changed, to notify of the initial value (this behaviour is also described here https://facebook.github.io/react-native/docs/appstate#basic-usage).

Demo

Here’s a Snack to demonstrate what I’m saying https://snack.expo.io/H1W9Lllg8…

By calling AppState in the render here, I’m triggering AppState to listen for changes earlier, without suffering from the first call handler call (although as said above is not something we can see in Expo Snack, but I used this to solve the same issue in my app and it works consistently everywhere)…

Solution

I have no idea whether this is something that should be taken with ReactNative to argue the design choice of AppState, but the solution for Expo could be to change this https://github.com/expo/expo/blob/master/packages/expo-web-browser/src/WebBrowser.ts#L134-L142 in the following way:

// Store the `resolve` function from a Promise to fire when the AppState
// returns to active
let _onWebBrowserCloseAndroid: null | (() => void) = null;

// Store previous app state to check whether the listener has ever been attached
let _previousAppState: null | string = AppState.currentState;

function _onAppStateChangeAndroid(state: AppStateStatus) {
  // if _previousAppState is null, we assume that the first call to
  // AppState#change event is not actually triggered by a real change
  // (https://facebook.github.io/react-native/docs/appstate#basic-usage)
  if (_previousAppState === null) {
    _previousAppState = state;
    return;
  }

  if (state === 'active' && _onWebBrowserCloseAndroid) {
    _onWebBrowserCloseAndroid();
  }
}

Please let me know if this is ok, I could make a PR or we could discuss further…


References

anyone in full 2022?

Not sure why this closed, I still experience the issue in production.

I’m experiencing this same problem when using WebBrowser. openAuthSessionAsync. Everything works in dev mode and in the production iOS app, but fails in the production Android app. Through logging I can see that { type: 'dismiss' } is returned before the browser even shows, just as described here. The suggested workaround of logging AppState.currentState unfortunately seems to no longer work. I’m really at a loss as to where to go from here - I can’t release our app without authentication on Android šŸ˜•

@brentvatne @ivansenic Even if there is no solution to this could we at least get this issue reopened as it is linked to in a few other issues? Over half the discussion has been after this issue was closed.

My kudos to @LucaColonnello, I rarely stabmle to such a quick and effective reaction on the bug ticket… Workaround provided, fix provided, what can you expect more šŸ‘ P.S. If you ever come to Belgrade I am taking you out for a drink, just ping me šŸ»

@schellack so if you confirm the patch I suggested works, I’m going to open a PR contributing here 😁

I’m having an issue with AuthSession as well, but with useAuthRequest. I described it on the Expo forum but mentioning it here in case anyone has any ideas. I tried the AppState.currentState ā€œtrickā€ but that didn’t work for me.

Hi @simplesthing ,

I was encountering the same issue as you on Android.

Using AuthSession.startAsync(), the result of this function always returned { type: 'dismiss" } even after successful authentication. I was able to get this fixed by supplying a returnUrl to the config object parameter passed to AuthSession.startAsync(). The AuthSession.startAsync() object parameters are outlined here.

It is strange but explicity supplying a returnUrl parameter, perhaps with a value of AuthSession.makeRedirectUri(), did the trick for me.

@LucaColonnello Thanks so much for your work on this. I have a question. Do you need to do anything other than update the contents of node_modules/expo-web-browser/src/WebBrowser.ts and republish the app? I still seem to have the double login issue on Android after doing this.

yes, I tried it with your changes.

@travishoki did you ever find a solution? Running into exactly the same issue with Google auth on Android standalone. Would love to know if you solved it.

@knorsen Yes, I was able to get it working. I’m using Expo 45 and expo-auth-session. Go checkout the answer from @XiangZhang0216 on this thread https://github.com/expo/expo/issues/10860. His suggested configurations in app.json did the trick.

I’ve been struggling with this for several hours. This solution worked for me.

Does it work with Google-auth ? I followed this step

This works fine on iOS standalone app but on Android every authentication attempt returns dismiss.

Can you share the example code ?

@tajetaje I upgraded to Expo SDK 45 and am still having issues with the standalone Android app. 😦

This is actually my issue: WebBrowser.openAuthSessionAsync returns ā€œdismissā€ even when the user successfully logged in #6289

On the Android standalone app, the login seems to work, but when coming back to the app it is returning with {type: ā€œdismissā€}. It works perfectly on iOS and in the emulator but not on the Android standalone app.

@viantirreau Great, thanks for sharing your solution. I’m not working on this part of the application at the moment but will remind myself to let it know here whether the solution works for me.

@YoranBrondsema I finally solved it using AuthSession.startAsync! I had to adapt this older Snack, but in the end it just worked fine for my purposes (OAuth). Just make sure to setup the REDIRECT_URL to something on the lines of https://auth.expo.io/@<username>/<app-name>. Although it’s more involved, as you have to manually configure some of the urls, I think you can make it work on itsme’s OIDC using this method.

Good luck!

@JuanDavidLopez95 Thank you for your response, I have read that issue as well, I am confused as to what the ./helpers file links to and what the getAuthorizeUrl is doing, I have searched for this in documentation I do not find anything. I am supplying a redirect uri and everything logs correctly.

my code :

const authUrl = state.settings.loginServiceUrl
const redirectUri = Linking.makeUrl('auth/in')
dispatch({type: 'log', payload: 'onLogin ' + AppState.currentState + ' ' + redirectUri})
try {
  const result = await startAsync({
    authUrl,
    redirectUri
   })
  dispatch({type: 'log', payload: `result: ${JSON.stringify(result)}`})
  } catch(e) {
    dispatch({type: 'log', payload: `error ${e}`})
  }

my url loginServiceUrl: https://${Domain}/login?client_id=xxx&redirect_uri=xxx&response_type=code&scope=xxx&state=xxx,`

EDITED: Finally got this resolved, a few things,

  • one I had the incorrect parameter, was using redirectUri instead of returnUrl

  • using any type of expo method to ā€œcreate a linkā€ like makeRedirectUri or Link.makeUrl all failed, manually typing in strings for the url’s worked

      const onLogin = async () => {
      const authUrl = state.settings.loginServiceUrl;
      dispatch({ type: "log", payload: "onLogin " + authUrl });
      try {
        const result = await startAsync({
          authUrl,
          returnUrl: "domain://auth/in?",
       });
      dispatch({ type: "log", payload: `result: ${JSON.stringify(result)}` });
      } catch (e) {
        dispatch({ type: "log", payload: `error ${e}` });
      }
      };
    

my url loginServiceUrl: https://${Domain}/login?client_id=xxx&redirect_uri=xxx&response_type=code&scope=xxx&state=xxx&return_url=xxx`

This same issue has been plaguing me for months now, but I had not yet been able to get to root cause. I used the patch to Expo WebBrowser that @LucaColonnello posted above, tested it in my app, and it seems to work perfectly.