Google 2FA breaks web login on expo android #676

Closed
opened 2026-03-13 07:59:57 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @etler on GitHub (Feb 15, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  • Using an android phone with an expo app using the expo better auth plugin.
  • Go to "System Settings > Apps".
  • Go to "Default Apps" and set your default browser to a browser other than Chome.
  • Go back and then to the see all apps list, navigate to the new browser set to default, go to "Storage & Cache" then tap "Clear Storage".
  • Go to the expo app with the better auth client and expo plugin and attempt to log in via the authClient.signIn.social({provider: "google"}) web sign in method.
  • Attempt to sign into google and trigger the OS native 2FA flow and tap allow.
  • After being sent back to the app, login will not succeed.

Here's a video of the issue on our app:

https://github.com/user-attachments/assets/21d3e2b1-763f-4b3e-9e33-86e3f37e4101

Current vs. Expected behavior

When the google 2FA flow inturrupts the web login window, it causes the login to fail.

The expected behavior is that the login should succeed as it does when google 2FA window is not required.

Login works normally when using a browser that has been trusted to not require 2FA on login.

What version of Better Auth are you using?

1.1.16

Provide environment information

- Device: Pixel 7 Pro
- OS: Android 15
- App Platform: React Native with Expo managed development build
- Plugin: Expo
- Default Browser: Confirmed failure on Firefox, Edge, and Samsung Browser

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

The config is unlikely applicable, but provided just in case:


betterAuth({
  appName: "Vetted",
  baseURL: host,
  basePath: "/auth",
  secret: sessionSecret,
  database: new Pool({
    connectionString: dbUrl,
  }),
  user: {
    // "user" is a reserved keyword in postgres so we are using plural names for tables
    modelName: "users",
    deleteUser: {
      enabled: true,
      afterDelete: async (user) => {
        await deleteMember(user.email)
      },
    },
  },
  session: {
    modelName: "sessions",
  },
  account: {
    modelName: "accounts",
    accountLinking: {
      enabled: true,
    },
  },
  socialProviders: {
    google: {
      clientId: googleClientId,
      clientSecret: googleClientSecret,
      scope: ["openid", "profile", "email"],
    },
  },
  plugins: [expo()],
  trustedOrigins: [siteHostname, mobileHostname],
  advanced: {
    cookiePrefix: cookieNamespace,
  },
  secondaryStorage: {
    get: async (key) => cache.get(key),
    set: async (key, value, ttl) => cache.set(key, value, ttl),
    delete: async (key) => cache.delete(key),
  },
})



createAuthClient({
  baseURL: `${authServerHostname}/auth`,
  plugins: [
    expoClient({
      scheme: "vettedapp",
      storagePrefix: "authClient",
      storage: {
        getItem: (key) => storage.getString(key) ?? null,
        setItem: (key, value) => storage.set(key, value),
      },
    }),
  ],
})

Additional context

We've encountered this same bug on our own with a passport based auth server and implementing our own expo login flow. The issue has also been posted in expo discussions. The reason why this happens is because the web browser being used for login is closed if the window loses focus. You can see it happen manually as well if you open the web browser via the auth flow, then switch apps to an unrelated app, then return to the login browser, the browser will close itself. Because the 2FA window is a separate app it causes the login web browser to close so the 2FA approval is never received by the login browser and the redirect back to the application with the credentials never triggers and login does not complete.

This is not an edge case because we discovered that it can happen very often on samsung phones. On samsung phones the samsung browser is set to the default browser, but google sign in sync is done via the chrome browser. This means that unless a user signs into google and trusts the samsung browser they will encounter this 2FA scenario and bug very often. In our analytics we found this impacted a lot of our samsung users.

There are 2 fixes neither which are ideal. To prevent the login session context from being lost you can pass in an additional option to the WebBrowser.openAuthSessionAsync call, { showInRecents: true }. This will tell the browser window to not automatically close itself and to remain in the recent application background so even if the login is interrupted the browser won't close and the login will succeed after 2FA is approved. The caveat is that after the login succeeds and takes you back to the app, the browser window will continue to exist in the background and needs to be manually closed by the user.

The other fix is to tell the WebBrowser.openAuthSessionAsync call to prefer using chrome, which is hopefully more likely to be trusted already, but that's not a guarantee, so while it makes it less likely that the 2FA window will appear, it cannot be relied upon as it might not be trusted, or chrome may not even be installed.

This is the WebBrowser.openAuthSessionAsync we had on our old auth flow:

        const { servicePackages } = await WebBrowser.getCustomTabsSupportingBrowsersAsync().catch(
          (err) =>
            err instanceof UnavailabilityError ? { servicePackages: [] } : Promise.reject(err),
        )
        const result = await WebBrowser.openAuthSessionAsync(
          `${authServerHostname}/auth/${provider}?source=mobile&path=auth`,
          Linking.createURL("/auth"),
          {
            browserPackage:
              servicePackages.find((packageName) => packageName === "com.android.chrome") ??
              servicePackages.find((packageName) => packageName === "com.sec.android.app.sbrowser"),
            showInRecents: true,
          },
        )

This is not ideal but having a browser window that needs to be manually closed is less bad than not being able to log in at all when 2FA triggers. Hopefully a better solution can be found. We are pairing better auth with Google Credentials Manager login which avoids this issue, however we fall back to better auth web login in the case that Google Credentials Manager is not available which should hopefully not be too often.

Originally created by @etler on GitHub (Feb 15, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce * Using an android phone with an expo app using the expo better auth plugin. * Go to "System Settings > Apps". * Go to "Default Apps" and set your default browser to a browser other than Chome. * Go back and then to the see all apps list, navigate to the new browser set to default, go to "Storage & Cache" then tap "Clear Storage". * Go to the expo app with the better auth client and expo plugin and attempt to log in via the `authClient.signIn.social({provider: "google"})` web sign in method. * Attempt to sign into google and trigger the OS native 2FA flow and tap allow. * After being sent back to the app, login will not succeed. Here's a video of the issue on our app: https://github.com/user-attachments/assets/21d3e2b1-763f-4b3e-9e33-86e3f37e4101 ### Current vs. Expected behavior When the google 2FA flow inturrupts the web login window, it causes the login to fail. The expected behavior is that the login should succeed as it does when google 2FA window is not required. Login works normally when using a browser that has been trusted to not require 2FA on login. ### What version of Better Auth are you using? 1.1.16 ### Provide environment information ```bash - Device: Pixel 7 Pro - OS: Android 15 - App Platform: React Native with Expo managed development build - Plugin: Expo - Default Browser: Confirmed failure on Firefox, Edge, and Samsung Browser ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript The config is unlikely applicable, but provided just in case: betterAuth({ appName: "Vetted", baseURL: host, basePath: "/auth", secret: sessionSecret, database: new Pool({ connectionString: dbUrl, }), user: { // "user" is a reserved keyword in postgres so we are using plural names for tables modelName: "users", deleteUser: { enabled: true, afterDelete: async (user) => { await deleteMember(user.email) }, }, }, session: { modelName: "sessions", }, account: { modelName: "accounts", accountLinking: { enabled: true, }, }, socialProviders: { google: { clientId: googleClientId, clientSecret: googleClientSecret, scope: ["openid", "profile", "email"], }, }, plugins: [expo()], trustedOrigins: [siteHostname, mobileHostname], advanced: { cookiePrefix: cookieNamespace, }, secondaryStorage: { get: async (key) => cache.get(key), set: async (key, value, ttl) => cache.set(key, value, ttl), delete: async (key) => cache.delete(key), }, }) createAuthClient({ baseURL: `${authServerHostname}/auth`, plugins: [ expoClient({ scheme: "vettedapp", storagePrefix: "authClient", storage: { getItem: (key) => storage.getString(key) ?? null, setItem: (key, value) => storage.set(key, value), }, }), ], }) ``` ### Additional context We've encountered this same bug on our own with a passport based auth server and implementing our own expo login flow. The issue has also been posted in [expo discussions](https://github.com/expo/expo/discussions/16735). The reason why this happens is because the web browser being used for login is closed if the window loses focus. You can see it happen manually as well if you open the web browser via the auth flow, then switch apps to an unrelated app, then return to the login browser, the browser will close itself. Because the 2FA window is a separate app it causes the login web browser to close so the 2FA approval is never received by the login browser and the redirect back to the application with the credentials never triggers and login does not complete. This is not an edge case because we discovered that it can happen very often on samsung phones. On samsung phones the samsung browser is set to the default browser, but google sign in sync is done via the chrome browser. This means that unless a user signs into google and trusts the samsung browser they will encounter this 2FA scenario and bug very often. In our analytics we found this impacted a lot of our samsung users. There are 2 fixes neither which are ideal. To prevent the login session context from being lost you can pass in an additional option to the `WebBrowser.openAuthSessionAsync` call, `{ showInRecents: true }`. This will tell the browser window to not automatically close itself and to remain in the recent application background so even if the login is interrupted the browser won't close and the login will succeed after 2FA is approved. The caveat is that after the login succeeds and takes you back to the app, the browser window will continue to exist in the background and needs to be manually closed by the user. The other fix is to tell the `WebBrowser.openAuthSessionAsync` call to prefer using chrome, which is hopefully more likely to be trusted already, but that's not a guarantee, so while it makes it less likely that the 2FA window will appear, it cannot be relied upon as it might not be trusted, or chrome may not even be installed. This is the `WebBrowser.openAuthSessionAsync` we had on our old auth flow: ```ts const { servicePackages } = await WebBrowser.getCustomTabsSupportingBrowsersAsync().catch( (err) => err instanceof UnavailabilityError ? { servicePackages: [] } : Promise.reject(err), ) const result = await WebBrowser.openAuthSessionAsync( `${authServerHostname}/auth/${provider}?source=mobile&path=auth`, Linking.createURL("/auth"), { browserPackage: servicePackages.find((packageName) => packageName === "com.android.chrome") ?? servicePackages.find((packageName) => packageName === "com.sec.android.app.sbrowser"), showInRecents: true, }, ) ``` This is not ideal but having a browser window that needs to be manually closed is less bad than not being able to log in at all when 2FA triggers. Hopefully a better solution can be found. We are pairing better auth with Google Credentials Manager login which avoids this issue, however we fall back to better auth web login in the case that Google Credentials Manager is not available which should hopefully not be too often.
GiteaMirror added the bug label 2026-03-13 07:59:57 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jun 11, 2025):

Hi, @etler. I'm Dosu, and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • Reported issue with Google 2FA interrupting the web login process on an Expo Android app.
  • Login fails when 2FA is required but succeeds without it.
  • Affects multiple browsers: Firefox, Edge, and Samsung Browser.
  • Occurs on a Pixel 7 Pro running Android 15 with Better Auth version 1.1.16.
  • No comments or developments have been made on this issue yet.

Next Steps:

  • Please confirm if this issue is still relevant to the latest version of the better-auth repository by commenting here.
  • If no further input is provided, the issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jun 11, 2025): Hi, @etler. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale. **Issue Summary:** - Reported issue with Google 2FA interrupting the web login process on an Expo Android app. - Login fails when 2FA is required but succeeds without it. - Affects multiple browsers: Firefox, Edge, and Samsung Browser. - Occurs on a Pixel 7 Pro running Android 15 with Better Auth version 1.1.16. - No comments or developments have been made on this issue yet. **Next Steps:** - Please confirm if this issue is still relevant to the latest version of the better-auth repository by commenting here. - If no further input is provided, the issue will be automatically closed in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#676