24

Our requirement is to have our users login to an app via a URL and, having added the app to their homescreen as a PWA, maintain that logged-in status, so that a second login to the installed PWA is not required. This is certainly possible under Android/Chrome where the logged-in status can be initially stored and accessed by the PWA via a variety of mechanisms (including cookie, IndexedDB, cache).

However, it now appears to us that a PWA under iOS 14/iPadOS 14 is tightly sandboxed and Safari has no way of passing logged-in status to it. Over the years, and through the various versions of iOS, a variety of sharing mechanisms have been offered - and rendered obsolete in a subsequent version. These include:

  1. the cache, accessed via a fake endpoint (ref)
  2. a session cookie (ref)

A mechanism that doesn't rely on browser-shared storage is the addition of a server-generated token to the URL (ref), (ref) - the problem here is that it upsets Android/Chrome, which uses an unmodified start_url in the web app manifest.

This is an issue which has provoked a number of SO questions over the years (three of them referenced above) and some of them have been answered with solutions that apparently worked under earlier versions of iOS. What we're wanting now is a solution which works under the latest version as well as it works under Android/Chrome. Any offers?

Velojet
  • 878
  • 11
  • 18
  • 5
    I note [an announcement on Discourse](https://meta.discourse.org/t/discourse-now-works-as-a-pwa-in-ios/146346) that states "Discourse now works as a PWA in iOS", with an associated FAQ: "Why do I have to login again in the PWA? Because the PWA instance doesn’t share cookies with main Safari on iOS". Which perhaps helps explain why my question still hasn't attracted an answer - despite noseratio's bounty (thanks, @noseratio ). – Velojet Jul 19 '20 at 03:52
  • 4
    No problem, I do think this question deserves more attention, especially in the context of the recent Maximiliano Firtman's [article](https://medium.com/@firt/think-lazy-ba26097cfdca). – noseratio Jul 19 '20 at 07:27
  • 2
    Thanks @noseratio for posting the link to Maximiliano Firtman's article on the still unresolved problems with Apple's implementation of PWAs,. I posted [a response](https://medium.com/p/ba26097cfdca/responses/show) to his article which focuses on the issue I've raised here. – Velojet Jul 19 '20 at 21:01

2 Answers2

7

It can be done. Here's how we've succeeded in doing it:

  1. When the user initially logs in to the app in the browser, we generate a UID on the server.
  2. We pair this UID with the username in a server file (access.data).
  3. We generate the web app manifest dynamically. In it we set the start_url to the index page and append a query string incorporating the UID e.g. "start_url": "/<appname>/index.html?accessID=<UID>".
  4. We create a cookie to verify that the app has been accessed e.g. access=granted.
  5. When the user accesses the app as an iOS PWA, the app looks for this cookie and doesn't find it (cunning ;) - we use one of the iOS deficiencies (not sharing cookies between Safari and the PWA) to defeat that same deficiency).
  6. The absence of the access cookie tells the app to extract the UID from the query string.
  7. It sends the UID back to the server, which looks for a match in access.data.
  8. If the server finds a match, it tells the app that the PWA user is already logged in and there's no need to again display the login screen. Mission accomplished!

Note: Android/Chrome simply ignores the accessID in the query string - I was wrong in my question to imply that Android/Chrome requires an unmodified start_url.

Velojet
  • 878
  • 11
  • 18
6

Generating Webapp manifest and changing start_url has it's own consequences.

For example Sometimes the data we want to pass is not available right away and also if data is passed in url we should make sure that passed login data are invalidated after first Webapp opening because otherwise sharing bookmark would also share login credentials of the user. By doing so you lose the power of start_url which means if users add your website when it's in subdirectory1 it would always open in subdirectory1 after that.

What's the alternative ?

Since ios 14, safari shares cacheStorage with Webapps. so the developer can save credentials as cache in the cacheStorage and access them inside the Webapp.

Code compatibility

About ios14 availability we should consider that before ios 14 more than 90% of users had updated to ios 13 and the fact that ios 14 is supported by all devices that support ios 13, we can assume that ios 14 usage would soon reach 90%+ and the other ~5% can alway login again inside Webapp. it has already reached 28% in 12 days based on statcounter

enter image description here Code example

Here is the code i use in my Webapp and it works successfully with ios add to home screen.

    ///change example.com with your own domain or relative path.
    function createCookie(name, value, days) {
      if (days) {
        var date = new Date();
        date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
        var expires = "; expires=" + date.toGMTString();
      } else var expires = "";
      document.cookie =
        name + "=" + value + expires + "; path=/; domain=.example.com";
    }
    
    function readCookie(name) {
      var nameEQ = name + "=";
      var ca = document.cookie.split(";");
      for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == " ") c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
      }
      return undefined;
    }
    
    
    async function setAuthFromCookie() {
      caches.open("auth").then(function (cache) {
        console.log("Set Cookie");
        return cache.add(["https://example.com/cacheAuth.php"]);
      });
    }
    async function setAuthToCookie() {
      var uid = readCookie("uid");
      var authKey = readCookie("authKey");
      caches.open("auth").then((cache) => {
        cache
          .match("https://example.com/cacheAuth.php", {
            ignoreSearch: true,
          })
          .then((response) => response.json())
          .then((body) => {
            if (body.uid && uid == "undefined") {
              /// and if cookie is empty
              console.log(body.authKey);
              createCookie("authKey", body.authKey, 3000);
            }
          })
          .catch((err) => {
            console.log("Not cached yet");
          });
      });
    }

    setTimeout(() => {
      setAuthFromCookie();
      //this is for setting cookie from server
    }, 1000);

    setAuthToCookie();
Pooya Estakhri
  • 1,129
  • 1
  • 11
  • 25
  • 1
    Thanks for your comments, Pooya Estakhri, and, in particular, for sharing your code. You make a fair point about the use of `start_url` in our approach. The advantage I'd claim for it is that is also works for the ~5% who'll still be on iOS 13 ;) – Velojet Oct 01 '20 at 21:21
  • 2
    After much consideration, we've decided to stick with our method. In our app, all the identified consequences of modifying `start_url` are dealt with: (1) the login data to be passed is available before the `start_url` rewrite; (2) the login data is invalidated immediately after its access to be checked; (3) because our app is a SPA, the potential for it to be added to homescreen when it's in a subdirectory doesn't exist. – Velojet Oct 10 '20 at 01:54
  • This approach does not work for cookie's set from the server with the httpOnly attribute. Any ideas on how to get around this? My initial thought was to create a special login endpoint on the server that accepts the token stored in the cookie, although maybe it's simply best to disabled the httpOnly property and not complicate things? – nolbuzanis Mar 01 '21 at 21:47