ionic-team / capacitor

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

CORS and cookies with capacitor:// scheme and XHR requests #1143

Closed rj33 closed 8 months ago

rj33 commented 5 years ago

After a bit of fiddling I had finally got cookies to get passed around on cross origin requests, but thus far I've been unable to do so on ios with capacitor:// and the latest release (1.0.0-beta.13).

Note that CORS seems to be working ok, I can do cross-origin requests to my remote server, and they work fine. What is not working is the sharing of cookies.

Note that there are several challenges with IOS and WebKit and cookie sharing. These are mentioned here: https://github.com/ionic-team/capacitor/issues/922

The first is that at some stage Apple changed the default cookie sharing policy (despite their docs saying they haven't). They changed the default from NSHTTPCookieAcceptPolicyAlways to NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain

It seems this default setting will refuse cross domain cookies not matter what you try to do with CORS.

So you need to do this: func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. HTTPCookieStorage.shared.cookieAcceptPolicy = HTTPCookie.AcceptPolicy.always return true }

In app/AppDelegate.swift

However, that isn't enough because there is also a webkit bug that doesn't initialise the cookie store the first time you launch the app, so you have to close and re-launch before doing anything cookie related. An ugly hack to work around this is to prod the cookie store before webkit starts with something like this: let cookie = HTTPCookie(properties: [HTTPCookiePropertyKey.domain: ".yourdomainhere.com", HTTPCookiePropertyKey.path: "/", HTTPCookiePropertyKey.name: "dummy", HTTPCookiePropertyKey.value: "dummy value"])! HTTPCookieStorage.shared.setCookie(cookie)

(placed in the same place as the the cookieAcceptPolicy setting above).

With those two bits of code, cookies worked for me in Webkit before the change to remove the server, and use capacitor://

I'm unable to make it work now, with a setup that seems to be the same cookie/cors treatment as being used in the pre-capacitor:// change situation.

I'm setting withCredentials on my XHR request (required for the cookies to be stored), but webkit is refusing to send cookies to the remove host.

Note that debugging this is a bit difficult because the same origin policy to reading cookies applies, so javascript can't check if the cookies exist on the client, the client when working will send any cookies back to the server, that the server set on previous request responses to the origin in question (capacitor://localhost in this case). The server seems to be fine, it appears to be sending the expected cookies and cross origin allowed headers, it is the webkit client that is deciding not to send them back.

I've tested this by adding the following code to my index.html:


       function success2() {
           console.log("xhr response2:",this.responseText);
       }
       function success() {
           console.log("xhr response:",this.responseText);
           var xhr2 = new XMLHttpRequest();
           xhr2.onload = success2;
           xhr2.onerror = error;
           xhr2.withCredentials = true;
           xhr2.open('POST', 'https://my.remotetestinghost.com/api/get_mobile_load_data');
           xhr2.send();
       }

       function error(err) {
           console.log('XHR Error:', err);
       }

       var xhr = new XMLHttpRequest();
       xhr.onload = success;
       xhr.onerror = error;
       xhr.withCredentials = true;
       xhr.open('POST', 'https://my.remotetestinghost.com/api/get_mobile_load_data');
       xhr.send();

Both requests succeed, and the server sets cookies on both, but the second request does not send back cookies to the server that it received on the first request (verified with safari network inspector attached to the emulator device).

I can't be sure I'm not doing something stupid somewhere, but it is unclear to me what. If this is a general problem and not something dumb at my end, I imagine this is a show stopper for a lot of people that are trying to wrap an existing web app, as remote server (cross domain) cookie session/auth is probably a very common case.

Is it possible the capacitor:// scheme method has broken cookie passing? It seems like it from my point of view. Is there anything I can do to bring it back? (other than reverting to the pre-capacitor) code.

I'm fairly sure it is a cross origin issue, because this code works ok in local dev mode where I'm going from capacitor://localhost to localhost:3000 (i.e. cookies are shared fine in that situation due to the same host name).

rj33 commented 5 years ago

Just a quick note on an easy workaround if you only need to communicate to one cross domain host, and don't need the access rights that come from calling hostname 'localhost'.

You can set: "server": { "hostname": "yourcrossdomainhost.com", }

It will still load all files from the local device, but now requests to yourcrossdomainhost.com will happily share cookies because it is no longer a cross domain request due to the device hostname being the same as the remote.

This isn't ideal, but it at least allows me to move forward without other hacks such as cookie injection and session id sharing across a network backchannel.

It would be nice if cross domain cookies just worked.

shuppert commented 5 years ago

@rj33 Where did you put the server/hostname changes?

beckmx commented 5 years ago

where do you set those lines of the hostname?

jcesarmobile commented 5 years ago

On the capacitor.config.json

beckmx commented 5 years ago

well, didnt work for me, is there any other way to let save cookies?

mkapnick commented 5 years ago

Any solution here? Pretty much blocks any progress I can make b/c of this :/

beckmx commented 5 years ago

I used one of the core libraries and let this library manage the cookies, of course your "services" or api calls will have to be written to be ready to be emulated in your local browser and run on the compiled ionic package

mkapnick commented 5 years ago

@beckmx Could you share some code? I'm using the ionic framework with capacitor, so I'm trying to understand how your solution would fit for my needs. Would be really helpful.

Angusoft-India commented 4 years ago

Please help me if there is any solution. Thanks in advance

Spambit commented 4 years ago

I think there is no such straight solution of this problem in capacitor as on today - at least in iOS. If you don't use capacitor and create your home grown solution that uses WKWebview with custom-schemes like myapp://localhost, you will face same problem. Cookies only get attached to subsequent requests if it is known schemes like - http/https. I am stuck in this same problem and don't think capacitor can fix it without substantial code changes. Hence I have used GCD server as proxy as it is used to be in old ionic. I think our home grown solution is perfect for all our testcases. One of us found this: https://sites.google.com/site/iosappnss/misuse-gcdwebserver-of-ios-app-a-case-study. I think this is the reason why they removed gcdserver. But, webstorages are something we must need to work with, otherwise how an app can log-in when lot of services are scattered in different domains. So, I am curious to know what exactly > e client when working wi -- you did?

IlCallo commented 4 years ago

My experience so far

When using server.hostname to make CORS work, all requests made with fetch requests against the same configured hostname return the native-bridge.js file, instead of proceeding to the remote server. For @rj33 this solution worked, I guess he had a different setup.

Case 1 config

{
  "appId": "org.company.my_app",
  "appName": "My App",
  "bundledWebRuntime": false,
  "npmClient": "yarn",
  "webDir": "www",
  "server": {
    "hostname": "subdomain.example.org",
    "androidScheme": "https"
  }
}

If server.allowNavigation contains the same domain configured as hostname, all requests will be forwarded to the remote server, so you'll actually get the webapp served from your webserver instead of the local one (if you have a webapp on the server), but in this case you cannot actually use Capacitor plugins in any way. This apparently works because of a Capacitor "bug" and it's not intended behaviour.

Case 2 config

{
  "appId": "org.company.my_app",
  "appName": "My App",
  "bundledWebRuntime": false,
  "npmClient": "yarn",
  "webDir": "www",
  "server": {
    "hostname": "subdomain.example.org",
    "androidScheme": "https",
    "allowNavigation": [
      "subdomain.example.org"
    ]
  }

The other alternative I found is the new HTTP plugin being developed, but it's in alpha and of course non production ready at all. The second scenario will work for us for now because we don't use Capacitor plugins and we are serving a PWA from the same server where API reside, but it's obviously unfeasible for almost all use cases.

@jcesarmobile Is there a way to avoid intercepting fetch/XHR requests (or provide a whitelist)?

rj33 commented 4 years ago

Sorry for those who asked for more details, I don't regularly monitor my github notificaitons.

@IlCallo

Hi, I'm not sure why your case1 setup worked for me, and not you. It still appears to be working for me as of the last release I used which was 2.1.2.

One of the challenges I had was that there was not just one thing stopping the cookies from working, but many things I had to fix before things worked, so what I thought was going wrong was not always the case.

On my setup where I set the server.hostname config, as my main method of dealing with all this, I also use config to define my XHR and websocket host destination so it is not the same the same as my hostname. I don't recall now exactly why I did that, but perhaps it was for the reason you mention that I needed different names to allow loading of the app assets to work from the local device. So I had something like "hostname": "mydomain.com"

and then I configured websocket and XHR requests to go to mobile.mydomain.com. They were same-domain so no cross domain cookie issue, and they were different to the hostname so no localhost targeting issues. Perhaps this detail is why it worked for me and not you. Sorry I should have mentioned it in my previous post.

I also have dev and production build differences with some simple config file/index.html templating so that the XHR/websocket and server.hostname settings can be set for localhost development and production setups separately.

Hope that helps.

yunuscanemre commented 3 years ago

also a problem for me. the only way to get the cookies to work was to set hostname in the server block which is not ideal. :(

beckmx commented 3 years ago

I used this wrapper to work on my localhost and to enable the calls in the mobile environment, you will have to use it instead of a direct call to the angular http or the native http:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HTTP } from '@ionic-native/http/ngx';
import { SettingsService } from '../services/settings.service';

@Injectable({
  providedIn: 'root'
})
export class CwHttpService {
  public httpClient: any;
  public isTestingMode = true;
  constructor(
    private ngClient: HttpClient,
    private nativeClient: HTTP,
    private settings: SettingsService
  ) {
    this.isTestingMode = this.settings.testingMode();
    this.httpClient = this.settings.testingMode()
      ? this.ngClient
      : this.nativeClient;
  }

  public get(url: string, data: any = {}) {
    console.log(url, JSON.stringify(data));
    const headers: any = this.settings.testingMode() ? {} : {
      'Set-Cookie': this.nativeClient.getCookieString(
        this.settings.realEndpoint
      )
    };
    return this.isTestingMode
      ? this.httpClient.get(url, data).toPromise()
      : this.httpClient.get(url, data, headers).then((response: any) => {
        return JSON.parse(response.data);
    });
  }

  public post(url: string, data: any = {}) {
    console.log(url, JSON.stringify(data));
    const headers: any = this.settings.testingMode() ? {} : {
      'Set-Cookie': this.nativeClient.getCookieString(
        this.settings.realEndpoint
      )
    };
    return this.isTestingMode
      ? this.httpClient.post(url, data).toPromise()
      : this.httpClient.post(url, data, headers).then((response: any) => {
        return JSON.parse(response.data);
    });
  }

  public put(url: string, data: any = {}) {
    const headers: any = this.settings.testingMode() ? {} : {
      'Set-Cookie': this.nativeClient.getCookieString(
        this.settings.realEndpoint
      )
    };
    return this.isTestingMode
      ? this.httpClient.put(url, data).toPromise()
      : this.httpClient.put(url, data, headers).then((response: any) => {
        return JSON.parse(response.data);
    });
  }
  public delete(url: string, data: any = {}) {
    const headers: any = this.settings.testingMode() ? {} : {
      'Set-Cookie': this.nativeClient.getCookieString(
        this.settings.realEndpoint
      )
    };
    return this.isTestingMode
      ? this.httpClient.delete(url, data).toPromise()
      : this.httpClient.delete(url, data, headers).then((response: any) => {
        return JSON.parse(response.data);
    });
  }
  public update(url: string, data: any = {}) {
    const headers: any = this.settings.testingMode() ? {} : {
      'Set-Cookie': this.nativeClient.getCookieString(
        this.settings.realEndpoint
      )
    };
    return this.isTestingMode
      ? this.httpClient.update(url, data).toPromise()
      : this.httpClient.update(url, data, headers).then((response: any) => {
        return JSON.parse(response.data);
    });
  }
}
dhaivat28 commented 3 years ago

I have the same issue i have been trying to read the message event from an iframe in capacitorjs and is definitely not a CORS issue as i checked response headers from server i.e Access-Control-Allow-Origin:* . After going through numerous posts i tried the most suggested solution to change the hostname. But it seems like it doesnt work in my case.

So my iframe is hosted at "subdomain.domain.com" so i changed the hostname in my capacitor.config.json to hostname:subdomain.domain.com and also tried setting hostname:domain.com but nothing works.

I believe the code is fine as it works on android and web and just doesnt work on capacitor's scheme. Incase you need to have a look at the code its posted here https://stackoverflow.com/questions/67464857/window-message-doesnt-work-on-capacitor-ios-after-deployment-to-testflight

if anyone can please suggest me a solution or alternative to make it work. Thanks in advance

jcesarmobile commented 8 months ago

This should no longer be an issue with http and cookies plugin that are part of Capacitor 5. If you are using them and still face CORS or cookie issue, please, create a new issue and provide a sample app that reproduces the problem

ionitron-bot[bot] commented 7 months ago

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Capacitor, please create a new issue and ensure the template is fully filled out.