Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IOS] Cookies problems in IOS #1373

Open
Dadoeo opened this issue Mar 29, 2019 · 95 comments
Open

[IOS] Cookies problems in IOS #1373

Dadoeo opened this issue Mar 29, 2019 · 95 comments

Comments

@Dadoeo
Copy link

Dadoeo commented Mar 29, 2019

Hi,

in Ios platform, Capacitor use the vkWebView, which has the well known problem of cookies shared between the domains, but no solution has been implemented.

In "cordova-plugin-wkwebview-engine", the ploblem is solved using "WKProcessPool":

"This plugin creates a shared WKProcessPool which ensures the cookie sharing happens correctly across WKWebView instances. CDVWKProcessPoolFactory class can be used to obtain the shared WKProcessPool instance if app creates WKWebView outside of this plugin."

Is possible add this on Capacitor? Or are there any workaround for do this manually?

Regards

@Dadoeo Dadoeo changed the title [IOS] Cookie problems in IOS [IOS] Cookies problems in IOS Mar 29, 2019
@jcesarmobile
Copy link
Member

Any information about how to reproduce the issue?

@Dadoeo
Copy link
Author

Dadoeo commented Mar 29, 2019

It's very simple, you can try to open console and do:

document.cookie = "username=John Doe";
document.cookie

Expected:
"username=John Doe"
But result is:
""

@jcesarmobile
Copy link
Member

Relevant PR where that was introduced apache/cordova-plugin-wkwebview-engine#27

@Dadoeo
Copy link
Author

Dadoeo commented Mar 29, 2019

thank you jcesarmobile.

Why was it not introduced even in Capacitor? How I do you put it manually?

@EiyuuZack
Copy link

It's very simple, you can try to open console and do:

document.cookie = "username=John Doe";
document.cookie

Expected:
"username=John Doe"
But result is:
""

I still get the same using WKWebView. On UIWebView this simple javascript works. Is document.cookie readonly on WKWebView? Any news on this?

@nfode
Copy link

nfode commented Jun 28, 2019

Are there any updates or workarounds? Thx in advance.

@miquelferrerllompart
Copy link

Hi, same problem here.
@EiyuuZack Do you have some workaround?

@shivatangirala
Copy link

shivatangirala commented Jul 6, 2019

Hi,

We have the same problem. We are using a third party JS framework which is performing the cookie test as mentioned below.

addTest("cookies", function() {
try {
t.cookie = "cookietest=1";
var e = t.cookie.indexOf("cookietest=") != -1;
return t.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT", e
} catch (e) {
return !1
}

As document.cookie is always returning "" . We are unable to use the third party JS framework.

I have created a singleton instance of WKProcessPool process and I have set it to webViewConfiguration.processPool = WKProcessPool().This didn't solve my issue as document.cookie is returning "".

Please let me know if there is any workaround for the problem.

Many Thanks

@mkapnick
Copy link

mkapnick commented Sep 4, 2019

@shivatangirala were you ever able to find an answer for this? Currently experiencing the same problem

@dminkovsky
Copy link

Currently the WKWebView does not accept/store/send XHR cookies. I believe this is currently an unsolved issue over in Cordova land as well.

@steel
Copy link

steel commented Jan 15, 2020

I ported over my Cordova app which was using https://github.com/oracle/cordova-plugin-wkwebview-file-xhr and it works here in Capacitor as well.

@alexsasharegan
Copy link

Is this tagged as low priority because it's out of scope for capacitor? This bug makes user sessions with cookies impossible, so the app I'm working on is unable to support persistent authentication, which is essentially the foundation of the whole app.

@steel
Copy link

steel commented Feb 5, 2020

The wkwebview file xhr plugin is what I’m using for the time being until some one figures this out.

@dminkovsky
Copy link

dminkovsky commented Feb 5, 2020

@alexsasharegan for now it seems like this approach is the easiest if your app is working against one host: ionic-team/cordova-plugin-ionic-webview#22 (comment).

I also do not understand how this issue could possibly be low priority. Is it because wkwebview is outside the scope/control of Capacitor? This problem forces to to either adopt the solution linked above, limiting you to one XHR host, or re-architecting your app to use authorization headers. I didn't have luck with the oracle plugin mentioned by @steel, but I would give it another shot if necessary given that some people report success.

@mlynch
Copy link
Contributor

mlynch commented Feb 29, 2020

Hi all, will take a look at this as part of #2495

@mlynch mlynch mentioned this issue Feb 29, 2020
11 tasks
@jcesarmobile
Copy link
Member

@mlynch, the original issue is about document.cookie not working (or returning "").
I don't think the HTTP plugin will be able to solve that.

I tested in the past (and again today) and the common process pool doesn't fix the issue on Capacitor, nor setting the cookies in the native side, they get set, but still not accessible on document.cookie.
I think it's related to the apps being loaded from a custom scheme (capacitor://). I've tested on cordova-ios 6 (dev version) and it has the same problem too since it also uses a custom scheme now instead of loading from file://

document.cookie works fine if I load an external website.

@mlynch
Copy link
Contributor

mlynch commented Apr 3, 2020

@jcesarmobile makes sense. This won't solve that problem but provides an easy way to get system wide cookies using the native Cookie management APIs which I think solves this

@anasvn
Copy link

anasvn commented Sep 10, 2020

Any solutions ? It is working in android without any issues.

@tafelnl
Copy link

tafelnl commented Oct 13, 2020

I have do have some ideas and code on how to fix this annoying issue.

Android cookies will work just fine as long as you have your webview server running on http://localhost and you have your backend hosted on a secure (SSL) endpoint. There is a little bug with the persisting of cookies when you close the app quickly after booting up. But there is a simple fix available. See: #3012

iOS cookies however... Well, they are something special.

Let's assume your main backend is running on https://subdomain.example.com

First of all you will have to add the following to your capacitor.config.json

  "server": {
    "hostname": "subdomain.example.com"
  }

mind you that this config should only be applied for ios

Then you should create a file which will handle the cookies for you. Let's call it CookiesPlugin.js

import '@capacitor-community/http';
import { Capacitor, Plugins } from '@capacitor/core';

const url = () => {
  return window.location.origin;
};

export const setCookie = (key, value) => {
  const { Http, CookieManagerPlugin } = Plugins;

  try {
    const parsedValue = JSON.stringify(value);
    value = parsedValue;
  } catch (e) {
    console.error(e);
  }

  if (Capacitor.getPlatform() === 'ios') {
    return CookieManagerPlugin.setCookie({
      url: window.location.host,
      key,
      value,
    });
  }

  return Http.setCookie({
    url: url(),
    key,
    value,
  });
};

export const hasCookie = async (key) => {
  const { Http } = Plugins;

  const ret = await Http.getCookies({
    url: url(),
    key,
  });

  return ret?.value?.some((cookie) => key == cookie.key);
};

export const getCookie = async (key) => {
  const { Http } = Plugins;

  const ret = await Http.getCookies({
    url: url(),
  });

  const value = ret?.value?.find((cookie) => key == cookie.key)?.value;

  try {
    const parsedValue = JSON.parse(value);
    return parsedValue;
  } catch (e) {
    console.error(e);
    return value;
  }
};

export const deleteCookie = (key) => {
  const { Http } = Plugins;

  return Http.deleteCookie({
    url: url(),
    key,
  });
};

As you can see, it mainly utilizes the https://github.com/capacitor-community/http plugin.

However their setCookie method is not fully working on iOS. So I made my own:

Add CookieManagerPlugin.swift:

import Capacitor

import Branch

@objc(CookieManagerPlugin)
public class CookieManagerPlugin: CAPPlugin {

  @objc public func setCookie(_ call: CAPPluginCall) {

    guard let key = call.getString("key") else {
      return call.reject("Must provide key")
    }
    guard let value = call.getString("value") else {
      return call.reject("Must provide value")
    }
    guard let urlString = call.getString("url") else {
      return call.reject("Must provide URL")
    }
    
    guard let url = URL(string: urlString) else {
      return call.reject("Invalid URL")
    }
    
    let jar = HTTPCookieStorage.shared
    
    let cookieProperties: [HTTPCookiePropertyKey : Any] = [.name : key,
                                                           .value : value,
                                                           .domain : urlString,
                                                           .originURL : urlString,
                                                           .path : "/",
                                                           .version : "0",
                                                           .expires : Date().addingTimeInterval(2629743)
                                                          ]

    if let cookie = HTTPCookie(properties: cookieProperties) {
        jar.setCookie(cookie)
    }
    
    call.resolve()
  }

}

together with CookieManagerPlugin.m:

#import <Capacitor/Capacitor.h>

CAP_PLUGIN(CookieManagerPlugin, "CookieManagerPlugin",
  CAP_PLUGIN_METHOD(setCookie, CAPPluginReturnPromise);
)

Both client and server side cookies will now fully work.

BUT, you will have to get and set them through the just created CookiesPlugin.js. For example:

import { setCookie, getCookie } from 'CookiesPlugin.js'

async () => {
  await setCookie('foo', 'bar');
  const result = await getCookie('foo');
  console.log(result); // outputs 'bar'
}

This means, that some third-party stuff will probably not work. See for example #1433 where Google Tag Manager (GTM) is not working. This is because GTM tries to set and get cookies by utilizing the document.cookies. But as said before it can only be done (on iOS at least) by using our custom CookiesPlugin.js. Of course all third-party plugins (like GTM) will not use this.

So another problem we need to tackle. I came up with an idea. It is partly working already, but Capacitor its core needs to be changed for this as well.

So what's the solution?

Apparently it's possible to proxy the document.cookie. See this answer on Stack Overflow: https://stackoverflow.com/a/33064438/8634342

This makes it possible to do something like this:

  Object.defineProperty(document, 'cookie', {
      set: function (val) {
        const values = val.split(/=(.+)/);
        if (values && values.length > 1) {
          setCookie(values[0], values[1]);
        }
      },
    });

What this does is the following: it replaces the browsers native setter for document.cookie with a custom one that utilizes the CookiesPlugin.js. So now every plugin ever that uses document.cookie to set cookies, will just work fine.

However, as I said before, there is still one catch: document.cookie will still return "". So writing cookies is possible for third parties, but reading them not. That can still cause problems. This can be solved, but capacitor does not (yet) allow for synchronous calls from JavaScript to Native code. That leaves us with the following issue:

It would be possible to do something like this:

  Object.defineProperty(document, 'cookie', {
      get: function (val) {
        const cookies = await getCookies();
        // parse the cookies, so they conform with the native document.cookie format
        return cookies;
      },
    });

But that await is not possible there. Hence, we need a synchronous function. So if and when Capacitor allows for synchronous features, as far as I am aware, every issue is tackled and we can have fully working cookies on both Android and iOS

@loburets
Copy link

loburets commented Nov 5, 2020

As it was said above, if you have issues that cookies are not attached to requests for ios because they are made to some other domain (not localhost), just use this advice:

ionic-team/cordova-plugin-ionic-webview#22 (comment)

But it works only for one domain, probably that one which you own, not third-party ones

And you also can get errors with some service like Stripe which require https to be used

@tafelnl
Copy link

tafelnl commented Nov 19, 2020

@jcesarmobile

Can you please take a look at this issue and update us about the priority and effort you guys are willing to put into this?

@jmschlmrs
Copy link

If your server.hostname and App Bound domains are set up properly, it may work but could have other unintended consequences

Thanks @thomasvidas for the reply, makes sense. Just confirming I was not able to get document.cookie working with App Bound Domains and server.hostname configured.

Would be interested in hearing from anyone that has.

@boycce
Copy link

boycce commented Dec 22, 2021

@thomasvidas

Safari does not support them, and Chromium will not support them in 2022. Third party cookies are going the way of the dodo and since the underlying webviews will auto-block third party cookies, Capacitor is somewhat limited in how it could potentially support third party cookies anyways.

This only pushes developers to use more complected methods of achieving the same result, and for Capacitor, something that should be working out of the box. In most cases developers only want to replicate "same site" cookie used in their web application, in their hybrid application.

Use tokens instead of session cookies if you can

Tokens still require a safe place to be stored, i.e. cookies. (UPDATE: Oh you meaning storing these first party cookies)

@abhilashsajeev
Copy link

@thomasvidas

Safari does not support them, and Chromium will not support them in 2022. Third party cookies are going the way of the dodo and since the underlying webviews will auto-block third party cookies, Capacitor is somewhat limited in how it could potentially support third party cookies anyways.

This only pushes developers to use more complected methods of achieving something that should be working out of the box for Capacitor. In most cases developers only want to replicate "same site" cookie used in their web application, in their hybrid application.

Use tokens instead of session cookies if you can

Tokens still require a safe place to be stored, i.e. cookies.

Seconded

@konnic
Copy link

konnic commented Mar 31, 2022

Is anyone aware of an exemplary public repo where the client- and server-side setting of cookies works?

@tafelnl you mentioned that with the workaround you described you're able to set cookies client- and server-side. Is that still the case? I'm not able to get it working. I've set the hostname in the capacitor config, am using the Http Plugin in combination with the native CookieManagerPlugin you described above and I set a respective AppBoundDomain, but it doesn't work for me. Do your workarounds still work for you?

@daniel-zero
Copy link

I fixed the issue by overwriting the cookie setter and getter functions and persisting the cookies by using the Capacitor Storage plugin. This will not solve 3rd party cookie issues, but it helped me to persist specific cookies I had to store in my application.

import { Injectable } from '@angular/core';
import { Storage } from '@capacitor/storage';
import { BehaviorSubject } from 'rxjs';

const COOKIES_CACHE_KEY = 'COOKIES_CACHE';

@Injectable({
  providedIn: 'root',
})
export class CookieStorageService {
  private _cookies = new BehaviorSubject<string>('');
  private _ready = new BehaviorSubject<boolean>(false);
  public ready = this._ready.asObservable();

  constructor() {
    this.loadCookiesFromStorage();
    this._cookies.subscribe((value) => this.persistCookies(value));
  }

  handleCookies() {
    Object.defineProperty(document, 'cookie', {
      set: (value) => this.saveCookie(value),
      get: () => this.getCookies(),
    });
  }

  private saveCookie(value: string) {
    const values = value.split(/=(.+)/);
    if (values && values.length > 1) {
      let currentCookies = this._cookies.value;
      currentCookies += value;
      this._cookies.next(currentCookies);
    }
  }

  private getCookies() {
    return this._cookies.value;
  }

  private persistCookies(value: string) {
    return Storage.set({ key: COOKIES_CACHE_KEY, value });
  }

  private async loadCookiesFromStorage() {
    const { value } = await Storage.get({ key: COOKIES_CACHE_KEY });
    this._cookies.next(value ?? '');

    if (!this._ready.value) {
      this._ready.next(true);
    }
  }
}
export class AppComponent {
...
// calling our 3rd party service that requires local stored cookies
this.cookieStorageService.ready.pipe(filter((value) => value)).subscribe(() => this.sourcePointService.displayConsentMessage());

@boycce
Copy link

boycce commented Apr 7, 2022

@gerritvanaaken for mine I used "hostname": "sub.my-domain.com" and this worked for both IOS and Android. where my-domain.com is the actual server endpoint and sub.my-domain.com is a subdomain that doesn't actually exist or route to anything.

I'm not sure that I'm fully grasping your question but this should work as long as the server is issuing cookies for my-domain.com then using a subdomain off of that would still be considered first party.

you can't use https://my-domain.com because android will direct api calls made to my-domain.com to itself or I believe that's what I was observing.

Thanks, a great workaround without waiting for hostnameAndorid / hostnameIOS settings

@avaleriani
Copy link

@gerritvanaaken can you share if and how you made it work on the ios simulator? I don't have a hostname, just localhost and cannot make it work

@qmarcos
Copy link

qmarcos commented Apr 25, 2022

@avaleriani it's all about this file: capacitor.config.json

More info on it here: https://capacitorjs.com/docs/config#schema

You have to add this entry to the capacitor.config.json:

  "server": {
    "hostname": "app-specific-subdomain.example.com"
  }

Assuming your main backend is running on https://example.com, this way on iOS the schema based on capacitor:// will work and on android you avoid the problem that appears when using the same domain that is used for the hostname (that impacts on how android try to load the url, because it understand is a local one)

Hope it helps.

@avaleriani
Copy link

@qmarcos Thank you for your very clear explanation. Works well for production. For IOS simulator I ended up using capacitor://localhost.

@inorganik
Copy link

For situations where you need cookie authentication in requests in your ionic app, the Community HTTP Plugin worked for me. I could not successfully set cookies on iOS even with that plugin, but it does allow you to manually set the cookie header, which no javascript api will. So you can do this:

import { Http, HttpResponse } from '@capacitor-community/http';

...

somePostRequest(): Promise<HttpResponse> {
    ...
    const options = {
      url,
      headers: { cookie: `myAuth=${token}` },
      data: {},
    };
    return Http.post(options);
}

@tafelnl
Copy link

tafelnl commented Aug 2, 2022

@thomasvidas
#1373 (comment)

For 4.0 we want to make cookies "just work" again, but we'd love to add it to 3.x if we can do it in a non-breaking way.

Now that Capacitor v4 is released, has this been fixed? I cannot really find what's new in Cap v4 in comparison to Cap v3?

@thomasvidas
Copy link
Contributor

thomasvidas commented Aug 2, 2022

Two things:

  • I am no longer part of the Capacitor team, got a new job recently! (The team still rules though!) So no idea when it will actually come, but...
  • We've communicated this on various other issues/forums/blogs, but not this issue. Native HTTP has been pushed back to 4.1, the team wanted to make sure that everything was working correctly and didn't feel comfortable with holding back the rest of 4.0 when native http can be non-breaking

@tafelnl
Copy link

tafelnl commented Aug 2, 2022

Congratulations on that 🥳 !

Thanks for taking the time to answer my questions despite that.

the team wanted to make sure that everything was working correctly and didn't feel comfortable with holding back the rest of 4.0 when native http can be non-breaking

Makes sense, I'll be waiting patiently ;)

@malte94
Copy link

malte94 commented Aug 13, 2022

I have a theoretical question. Many libraries or external services of course do not use some Http Plugin from Capacitor or the Cookies plugin to store cookies. Nevertheless, there are many of those that also drop necessary cookies.

Now when 4.1 is released: Will this all work "out of the box" or only when I use the plugins provided by Capacitor, so implement stuff myself?

Thank you! :)

@rsculati
Copy link

is there any updates on this as we're now at version 4.3.0 ?

@tsenguunchik
Copy link

Even after using CapacitorCookie and CapacitorHttp plugins, I'm still having an issue. Anyone successfully got it working?

@malte94
Copy link

malte94 commented Nov 7, 2022

@tsenguunchik Pointing to a server where the app is hosted is currently the easiest solution. Although not recommended, since it does not really fulfill the purpose of a "native" app.

@jwisser-heap
Copy link

Cookies appear to work as expected for me in a new Capacitor app with CapacitorCookies enabled as of 4.6.x, with 4.6.0 being the first to correctly set and persist cookies on iOS/WKWebView thanks to this commit. Curious if other folks still see issues in apps created using 4.6.0 or later.

@cmubo
Copy link

cmubo commented Jan 12, 2023

EDIT/UPDATE: So I removed the WKAppBoundDomains from the info.plist that I had put there, and now both setting and getting cookies works. The cookies are readable when setting the cookie locally and also using the set-cookie header in a request.

For context, I have set a hostname which is a subdomain of the cookie domain. No other config changes, no changes to the info.plist either.


@jwisser-heap I'm still getting the same issues on 4.6.1. Latest versions of the http and cookies plugins too. IOS, cant read any cookies.

set-cookie header works and I can see it in the cookies tab in safari inspector but it cant be read.

@aldencolerain
Copy link

aldencolerain commented Jan 15, 2023

@cmubo I am confused how you are able to read cookies set by the server. I wish I could but I don't understand how this will ever be possible with the capacitor:// schema.

From what I understand document.cookie will only read cookies set by the server on the same domain (possibly subdomain) but, cookies with a different schema are never considered the same domain according to mozilla docs.

This is a problem for me because we have our mobile app making api requests to a Django backend at api.example.com and we have our app set to the host app.example.com. We have 2 cookies, an HTTP only session cookie and a CSRF cookie.

The session cookie is set by the server and is sent back to the server every time we make an API call. This works great. The session is unreadable by document.cookie, but that's expected because its set to HTTP only.

The CSRF cookie is set by the server. We set the DOMAIN attribute correctly to subdomains are considered the same domain. However do CSRF we need to read the cookie and send that value as a header with each post request. Unfortunately this only works on Android because on iOS the schema doesn't match and the cookie is considered a third party cookie and is unreadable.

[UPDATE]
It looks like enabling the Cookies plugin's native patch allows the cookies with the different capacitor:// schema to be read in iOS even though they are technically different "domains" because the schemas don't match.

However, for some reason the capacitor:// context is considered insecure i guess? And the CSRF cookie could only be read even with the plugin patch if I set it to insecure. I really stumbled into this, it would be nice if someone would add these details/workarounds in more clarity to the docs. The cookies stuff is just such a critical issue for any app even those using JWT need to store in cookies.

@stepro95
Copy link

stepro95 commented Aug 10, 2023

Is there any updates to this issue? I'm still not able to set cookies in iOS 16 on version 5.2.2.

@kolja-ec
Copy link

kolja-ec commented Sep 6, 2023

Reads like a show-stopper... any update? We are on 5.0.0 - still not working...

@muuvmuuv
Copy link

In our case, when using Browser plugin, the main problem is that it uses SFSafari instead of ASWebAuthenticationSession to share cookies and so on. So we cannot do any SSO with this plugin to share server cookies.

@pjamessteven
Copy link

My app uses session/cookie based authentication I was having issues with cookies being persisted between app restarts on iOS. I solved this issue by simply importing the CapacitorCookies module in my root component (App.vue).

import { CapacitorCookies } from '@capacitor/core';

I think that by importing the package, some kind of initialization/restoring of cookies occurs. Or maybe it doesn't get bundled properly by vite if it's not defined as an import. You don't need to actually use and methods of the package, and it will show as unused in your IDE.

I'm using Capacitor 5 (I'm using 5.4.2 to be specific) with Vue 3 and vite, hope this helps.

The rest of my config is as follows:

{
  "appId": "org.website.com",
  "appName": "Website",
  "server": {
    "hostname": "api.website.com"
  },
  "bundledWebRuntime": false,
  "npmClient": "yarn",
  "webDir": "www",
  "plugins": {
    "CapacitorCookies": {
      "enabled": true
    },
    "CapacitorHttp": {
      "enabled": true
    }
  },
  "ios": {
    "limitsNavigationsToAppBoundDomains": true
  }
}

@Index-s
Copy link

Index-s commented Feb 15, 2024

Hello, is there any news on this issue? I have this problem with android, tested with multiple versions. The main issue seems to be solved in capacitor 6, at least for the newer android versions from API 33 upwards, the others versions still have the issue, which is not understandable. Does anyone has any workarounds for that? I always read about using http and cookies plugin, but the docs are not really intuitive. Can someone give some example on where exactly or how exactly to do the xhr request with the saved cookies?

@rricamar
Copy link

+1

I'm employing an HTTP-only cookie to securely access an API that demands authentication. According to the documentation, enabling the CapacitorCookies plugin, alongside the WKAppBoundDomains in the Info.plist (with localhost and api.service.com), and setting limitsNavigationsToAppBoundDomains should suffice. However, this approach doesn't work until I change the server.hostname to api.service.com, which is discouraged.

I'm unsure if I'm missing something... or if somebody could assist here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.