Open Dadoeo opened 5 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.
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.
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.
@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 ?
@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?
@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...
@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.
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.
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!
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!
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.
@alexandermorgan This issue is about cookie support in iOS; it's not about the Capacitor storage plugin. For Capacitor support, try the Capacitor forum.
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",
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
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?
@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
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:
at_check=true
, mbox=session#id
which are not large and can be public, but it may break in some edge cases that I can't think of right nowHttpOnly
cookies is localStorage (or even just create a mechanism to expose those via javascript), it would be insecure. Best option would be for native code to attach that information for requests, but can it happen without involvement of javascript?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)
@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
?
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.
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.
@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)
@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
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?
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());
@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
@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
@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.
@qmarcos Thank you for your very clear explanation. Works well for production. For IOS simulator I ended up using capacitor://localhost
.
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);
}
@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?
Two things:
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 ;)
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! :)
is there any updates on this as we're now at version 4.3.0 ?
Even after using CapacitorCookie and CapacitorHttp plugins, I'm still having an issue. Anyone successfully got it working?
@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.
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.
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.
@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.
Is there any updates to this issue? I'm still not able to set cookies in iOS 16 on version 5.2.2.
Reads like a show-stopper... any update? We are on 5.0.0 - still not working...
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.
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
}
}
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?
+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
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