react-native-cookies / cookies

🍪 Cookie Manager for React Native
MIT License
479 stars 95 forks source link

Cookies not being sent to webview for iOS #188

Open caralin3 opened 6 months ago

caralin3 commented 6 months ago

Bug description:

Web view should be rendering a web page that has a cookie authentication. Instead it is rendering sign in page. This is happening on iOS after I upgraded to react native 0.73+. It was working properly before the upgrade. Android is working fine.

I have both thirdPartyCookiesEnabled={true} and sharedCookiesEnabled={true} enabled. Also in handleNavigationStateChange I reset the cookies for iOS:

async function resetCookies() {
    const cookies = await CookieManager.getAll(Platform.OS === 'ios');
    Object.keys(cookies).forEach(key => {
      const cookie = cookies[key];
      if (cookie.domain === domain) {
        CookieManager.clearByName(url, cookie.name, Platform.OS === 'ios');
      }
    });
  }

I also tried to sync the cookies manually with this in the onLoad and the onNavigationStateChange of <WebView />, but it still didn't work.

  const synchronizeCookiesiOS = async () => {
    if (Platform.OS === 'ios') {
      try {
        const allCookies = await CookieManager.getAll(true);
        for (const cookieKey of Object.keys(allCookies)) {
          const cookie = allCookies[cookieKey];
          if (cookie) {
            await CookieManager.set('https://' + cookie.domain, cookie);
          }
        }
      } catch (error) {
        console.error('Error synchronizing cookies:', error);
      }
    }
  };

Expected behavior:

Web view should render account page, not sign in page

Environment:

karel-suchomel-ed commented 5 months ago

One thing that could be causing this is that you are not setting the webkit boolean to true here: await CookieManager.set('https://' + cookie.domain, cookie); In resetCookies function you are reading those cookies from WebKit WKHTTPCookieStore storage. Also, you should turn sharedCookiesEnabled off for ios when using WebKit.

caralin3 commented 5 months ago

Thanks @karel-suchomel-ed! That fixed it for the initial Webview load, but when I leave the webview and then return to it, the cookies are not being synced and I am back to the sign in screen.

Any ideas on why re-entering the Webview, the cookies don't work?

<WebView
          style={styles.webview}
          source={{ uri: url }}
          javaScriptEnabled
          domStorageEnabled
          startInLoadingState={false}
          injectedJavaScript={script}
          onLoad={synchronizeCookiesiOS}
          onMessage={() => {
            // nothing
          }}
          onNavigationStateChange={handleNavigationStateChange}
          thirdPartyCookiesEnabled={true}
        />
karel-suchomel-ed commented 5 months ago

I think that for your use case you could use something similar that I used for sharing sessions between RN and WebView. In our app we had a PHPSESSID cookie encoded in a JWT token and this worked for me (not 1:1 code):

const Example = () => {
  ...
  const [isReady, setIsReady] = useState(false)

  useFocusEffect(
    useCallback(() => {
      const getCookieHeaderString = async () => {
        const token = getUserAccessToken()
        if (token) {
          const cookie = getCookieFromToken(token)

          if (Platform.OS === 'android') {
            await CookieManager.clearAll(useWebKit)
          }

          await CookieManager.set(
            url,
            {
              name: 'PHPSESSID',
              value: cookie.sub,
              httpOnly: true,
              secure: true
            },
            useWebKit
          )
        }
        setIsReady(true)
      }

      if (customer) {
        getCookieHeaderString()
      } else {
        setIsReady(true)
      }

      return () => {
        setIsReady(false)
      }
    }, [customer])
  )

  if (isReady) {
    return null
  }

  return (
    <Webview
      ...
    />
  )
}

The key was to set the cookie before every webview mount. I didn't experience any false positives this way, even tho the documentation warns about this. If you need to persist browser history between WebView mounts, you could write your own logic and handle the URL with state. I was using this hook for managing the history stacks:

export default function useBrowserHistory(homepage: string) {
  const [backStack, setBackStack] = useState<string[]>([homepage])
  const [forwardStack, setForwardStack] = useState<string[]>([])

  const visit = useCallback(
    (url: string) => {
      if (!backStack.includes(url)) {
        setForwardStack([])
        setBackStack((prev) => [...prev, url])
      }
    },
    [backStack]
  )

  const back = useCallback(
    (steps: number = 0) => {
      let i = steps
      const tempBackStack = [...backStack]
      const tempForwardStack = [...forwardStack]
      while (tempBackStack.length > 1 && i-- > 0) {
        tempForwardStack.push(tempBackStack[tempBackStack.length - 1])
        tempBackStack.pop()
      }
      setForwardStack(tempForwardStack)
      setBackStack(tempBackStack)
      return tempBackStack[tempBackStack.length - 1]
    },
    [backStack, forwardStack]
  )

  const forward = useCallback(
    (steps: number = 0) => {
      let i = steps
      const tempBackStack = [...backStack]
      const tempForwardStack = [...forwardStack]
      while (tempForwardStack.length > 0 && i-- > 0) {
        tempBackStack.push(tempForwardStack[tempForwardStack.length - 1])
        tempForwardStack.pop()
      }
      setForwardStack(tempForwardStack)
      setBackStack(tempBackStack)
      return tempBackStack[tempBackStack.length - 1]
    },
    [backStack, forwardStack]
  )

  const resetHistory = useCallback(() => {
    setBackStack([])
    setForwardStack([])
  }, [])

  return {
    visit,
    back,
    forward,
    backStack,
    forwardStack,
    resetHistory
  }
}

I only reset the cookies for Android, because I had some issues overwriting other cookies there.

You should also store that session cookie in secure storage (MMKV for example), so you can set it before WebView load, because session cookies are wiped. I took inspiration from this: https://stackoverflow.com/questions/62057393/how-to-keep-last-web-session-active-in-react-native-webview but instead of passing the cookie through cookie header in source I set it via the CookieManager.