ionic-team / capacitor

Build cross-platform Native Progressive Web Apps for iOS, Android, and the Web ⚡️
https://capacitorjs.com
MIT License
12.19k stars 1.01k forks source link

[IOS] Cookies problems in IOS #1373

Open Dadoeo opened 5 years ago

Dadoeo commented 5 years ago

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

tafelnl commented 3 years ago

@thomasvidas Thanks for chiming in and expressing the point of view and status at this moment.

Your suggestion about using tokens instead of session cookies is completely valid. Except you forget about the fact that tokens too should be stored somewhere secure. One way of storing tokens securely in a web app is, well, by using cookies (HttpOnly, Secure). And that's exactly the crux here; it is not very doable at this very moment.

As I stated before there are ways to achieve secure storage of tokens without the use of cookies. But I think Capacitor should take a stand in this and explain step by step how to achieve this. Because some caveats will lurk around the corner. By just missing one simple thing you might expose the tokens of your users for the world to see. After all, anything that can go wrong, will go wrong.

It will be very helpful to have at least some concrete guidelines, so every Capacitor developer will use the same best practice regarding authentication.

DCurtin commented 3 years ago

So not using third party cookies would mean switching the hostname to be the same domain as the api server, correct?

From my understanding there were some shortfalls to switching hostname from localhost to anything else related to accessing things as NKZ15 brought up in their comment.

image

is there a way to mitigate the issue around changing hostname so we can use first party cookies? That seems to be working fine for me on both android and IOS.

gerritvanaaken commented 3 years ago

@NKZ15 @DCurtin I understand that we can have different URL schemes for iOS and Android. But in my case I’d need different hostnames.

Can I have both in capacitor.config.json ?

DCurtin commented 3 years ago

@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.

do you need localhost to be the hostname on android for any particular reason? like what's outlined in that config file that I screen-capped from NKZ15 or were you having trouble getting api calls to work after changing it?

gerritvanaaken commented 3 years ago

@DCurtin Thanks for sharing your setup! Well, mine is really similar. I also stumbled upon the "Android routes to itself" issue, when using "mydomain.com" as base url.

I have two different APIs to talk to, one of which is Origin-whitelisted, so I can only have mydomain.com and localhost, and as my app also has to work as a PWA, this is getting difficult.

Should be a piece of cake for the Ionic team to add something like "iosHostname" and "androidHostname" to the config file. Until then I have to outcomment stuff manually before syncing the native codebase...

tafelnl commented 3 years ago

@gerritvanaaken I am afraid that this (edit: by this I mean the dynamic config) is a wont-fix for the Capacitor team. One solution would be to have a dynamic config, and as far as I can remember they won't introduce it to Capacitor 2.0. But it is actually available in Capacitor 3.0. So if you are on Capacitor 3.0 you could do it automatically. Otherwise you should change it every time you build an app for iOS. But I think this discussion is a little out of scope for this issue anyway as it is more config related than cookie related.

thomasvidas commented 3 years ago

To clarify the wont-fix comment, if it really was a wont-fix issue the Capacitor team would just close the issue, lock comments, and call it a day. In order to properly fix this, we'd need to add a synchronous API from the web layer to the native layer; which is a pretty big feature that we haven't properly scoped out yet. It is a little disingenuous to just imply this is a wont-fix issue.

There are workarounds that I commented on earlier and you can also set the server.hostname to use document.cookie as described above, but you lose secure context and will lose access to certain Web APIs.

tafelnl commented 3 years ago

I am sorry @thomasvidas for the confusion. I see how my wording can be confusing. I actually meant the dynamic config issue to be a wontfix. Not this issue. I will edit my comment above to point that out. I know you guys are working hard and I appreciate that!

tafelnl commented 3 years ago

I would also like to point out that dsbridge already has a synchronous api as I said in #3675. I dont know if it is even applicable to capacitor. But maybe it is helpful to you guys. Thanks for all the efforts you guys are putting into the community!

alexandermorgan commented 3 years ago

You can use @capacitor/storage in Cap 3 as follows, it's easy!

Does anybody know of a real open-source repo that uses @capacitor/storage? I searched on github but couldn't find one. I've tried so many things and I just can't get Storage to work for Android (haven't tried iOS yet). The plugin imports but just as an empty object. There is this blog post by Joshua Morony but it uses the Capacitor 2 syntax and more importantly I also can't get it to work. Capacitor is really an incredible tool so I would love to be able to keep using it, I just must be missing something basic because I can't get even a toy example to work for the @capacitor-community/http, Storage, or Geolocation plugins.

ptmkenny commented 3 years ago

@alexandermorgan This issue is about cookie support in iOS; it's not about the Capacitor storage plugin. For Capacitor support, try the Capacitor forum.

KonkypenT commented 3 years ago

The matter is that the domain names do not match in the application. We need to modify the capacitor.config.json file and add the following:

{
  "appId": "io.ionic.started",
  "appName": "appName",
  "webDir": "www",
  "bundledWebRuntime": false,
  "server": {
    "hostname": "youdomainname.com"
  }
}

But in this case, there is a problem for those who may have several backends, as I had and how they write about it here.

How we solved it: In package.json, you need to configure the assembly in such a way that, before building the application, modify the capacitor.config.json file in the way we need. And so, add 2 new scripts to package.json:

"prebuild:tst": "cross-env NODE_ENV=tst node prebuild.js",
"prebuild:prod": "cross-env NODE_ENV=prod node prebuild.js",

cross-env is the package to install: npm i cross-env.

Fine! We now have a build command. Let's write the script itself to change the file. In the same directory where package.json is located, create a prebuild.js file and write in it:

const fs = require('fs');

const fileOriginal = fs.readFileSync('capacitor.config.json', 'utf8');
const url = process.env.NODE_ENV === 'tst' ? 'youdomainname1.com' : 'youdomainname2.com';

const obj = JSON.parse(fileOriginal);
obj.server = { hostname: url };
fs.writeFileSync('capacitor.config.json', JSON.stringify(obj));

Next, in the build command, write the following: "build:test:ios": "npm run prebuild:tst && ionic capacitor build ios -c=test --device --release",

webuniverseio commented 2 years ago

Maybe a silly idea, but let me ask this - what if we proxy cookie support with localStorage instead? It is a synchronous api... You should be able to implement expiration by scheduling expiration check to a closes to expire cookie and do that again when one expires. Not that it would be a very fun task to implement and you may loose localStorage between app updates, but it may be closest to what we can get without sacrificing secure context and apis like geolocation.

Even for cookies that are set by api calls/page load it should be possible to get that data either from native bridge or js code

webuniverseio commented 2 years ago

Also can someone 🙏 explain why document.cookie work on Cordova (maybe with a plugin mentioned in the very first post in this issue) and there isn't a problem with sync/async, but it is a problem with Capacitor?

KonkypenT commented 2 years ago

@webuniverseio I don't think this is a good idea. Cookies can be sent along with the request without unnecessary body movements. There is no need to implement the cookie retention period and worry about the fact that when we update the application, we will lose some data. They can also be made secure so that the client cannot access the cookies. If the first two points can be implemented with local storage, then the last one, I'm not sure if it is possible

webuniverseio commented 2 years ago

Thank you @KonkypenT, you're right that in addition to being able to read values from document.cookie, browser will also include cookie headers in network requests. We could solve that via a js wrappers around xhr and fetch apis - it wouldn't work for everything, like tags which load content from src attributes but I doubt that is a concern for majority of use cases. So experience can still be seamless. For HttpOnly you may still simulate behavior and only include them in async requests and don't expose that info via document.cookie getter. Same idea for Secure option. Not sure about SameSite, need to take a look into that. Since requests will likely be asynchronous you may even retrieve that data from native storage if that makes it any more secure.

Not sure why retention period is not important, I think if something is expected to expire, it should. Agree about updates, it seems that that was resolved as part of https://github.com/ionic-team/capacitor/issues/636

A couple of caveats:

webuniverseio commented 2 years ago

Another possibly silly question. Assuming you plan to deploy a website, not just a hybrid application, you'll likely have a https://my-cool-site.com. You can specify that site in https://capacitorjs.com/docs/config server.url and app would load without cookie issues. Yet documentation says:

This is intended for use with live-reload servers
**This is not intended for use in production.**

I don't understand why is that the case? What is so unusual from using a live webview in an app? Yes startup may be slower - but hopefully only first time. Yes you now need to rely on network caching instead of files served from local server. Are there other reasons why documentation says you should not used that in production? It definitely fixes the problem with cookies and other issues like non standard protocol which breaks recaptcha & google analytics or any urls which start with //

Edit: one of the issues I run into is when you specify server.url which supports service workers and offline mode, for example https://app.ft.com/stream/home, you get about:blank url instead of webview trying to load site. If you test how Safari is working, you get app working even after app restart. Unfortunately that is not the case with Capacitor, not sure if there is a workaround for that which can also support cookies. On Android if you use a server.url and website supports offline mode you don't get that problem

Edit2: looks like this is not a silly question and apps can be loaded live in production https://github.com/ionic-team/capacitor/issues/4122. That issue also mentions how to enable service workers support so you can have offline applications (and better cached applications)

jmschlmrs commented 2 years ago

@thomasvidas or @tafelnl are either of you able to clarify the root cause for document.cookie get() and set() silently failing and always returning an empty string ""?

It seems like we have valid workarounds for most other use cases based on both your earlier comments (thanks!), but there is still the case where there is some third-party or otherwise difficult to modify javascript code that assumes both reading and writing from document.cookie works.

I thought that setting server.hostname in capacitor.config to match the server hostname might allow the JS document.cookie API to work, but no such luck.

Do we know/assume that it is because of the custom protocol, e.g. capacitor://localhost?

thomasvidas commented 2 years ago

It's several things, the main one being it was a deliberate change from Apple on iOS 14 and up called "Intelligent Tracking Prevention" (ITP) which disables all cookies on domains not listed as an App Bound Domain. It's not due to the capacitor:// protocol. ITP made it so document.cookie calls were intended to silently fail to prevent user tracking. If your server.hostname and App Bound domains are set up properly, it may work but could have other unintended consequences (such as Apple potentially rejecting your app) so we don't recommend it.

The team has been tinkering with potentially adding a @capacitor/cookie plugin, but we haven't had much luck with properly patching document.cookie since all of Capacitor's APIs are async and document.cookie is sync. There's another issue for that, but I'm not sure it's possible without some pretty hacky code using prompt(). We have a couple proof of concept versions that fix this issue, but so far they are all breaking changes.

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.

jmschlmrs commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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

inorganik commented 2 years ago

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 commented 2 years ago

@thomasvidas https://github.com/ionic-team/capacitor/issues/1373#issuecomment-991425514

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 commented 2 years ago

Two things:

tafelnl commented 2 years ago

Congratulations on that :partying_face: !

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 commented 2 years ago

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 commented 2 years ago

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

tsenguunchik commented 2 years ago

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

malte94 commented 2 years ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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

muuvmuuv commented 1 year ago

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 commented 10 months ago

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 commented 9 months ago

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 commented 8 months ago

+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