Bug 241803

Summary: Safari throws exception when calling requestStorageAccess
Product: WebKit Reporter: Jason Wilson <jason.wilson>
Component: Website StorageAssignee: Nobody <webkit-unassigned>
Status: RESOLVED WORKSFORME    
Severity: Normal CC: sihui_liu, wilander
Priority: P2 Keywords: DoNotImportToRadar
Version: Safari 15   
Hardware: Unspecified   
OS: Unspecified   

Description Jason Wilson 2022-06-21 06:59:34 PDT
I have to say Safari's (webkit) implementation of Intelligent Tracking Protection (ITP) and the Storage Access API has been challenging to get right.

Situation:
- We have a company that has grown through acquisition and we are trying to implement a unified authentication scheme that uses cross-domain access to tokens stored in cookies 
- Each portal implementing the scheme will have an iframe that hosts a component from an authentication domain and will use **postMessage()** to check for the existence of the necessary authentication token.
- The initial implementation worked for Chrome/Edge/Opera/other Chromium browsers, but needed to be adjusted to implement the Storage Access API to allow the authentication component to request 1st party storage access.
- This worked as documented in Firefox
- Safari throws an exception when **requestStorageAccess()** is called and the error object is undefined

Here is some examples of the relevant code:

**Iframe**
``` html
<iframe  class="portal-navigation-frame" allowtransparency="true" style="position:absolute; top: -60px; right: -250px;display:none;"
                id="authFrame" sandbox="allow-scripts allow-storage-access-by-user-activation allow-same-origin allow-top-navigation allow-forms"
                src="@Constants.AuthenticationUrl"></iframe>
```

**Authentication Component**
``` javascript
const authorizeStorageAccess = async () => {
  if (document.hasStorageAccess) {
    try{
      if (await document.hasStorageAccess() == false) {
        console.log("authCommunicationService.authorizeStorageAccess", "does not have storage access");
        if (document.requestStorageAccess) {
          await document.requestStorageAccess();
        } else {
          console.log("authCommunicationService.authorizeStorageAccess", "requestStorageAccess not available");
        }
      }
      else {
        console.log("authCommunicationService.authorizeStorageAccess", "already has access");
      }
    }
    else {
      console.log("authCommunicationService.authorizeStorageAccess", "already has automatic 
  access");
    } catch (err) {
      console.log("authCommunicationService.authorizeStorageAccess", "error", err);
    }
  }
};
```

Note:  **authorizeStorageAccess()** is called from a button event handler and only after the user has been redirected to the authentication domain to login and returned.

Any assistance would be greatly appreciated.

Jason
Comment 1 John Wilander 2022-06-21 08:26:58 PDT
Hi! Thanks for filing.

We have published a guide to help with adoption of the API. See "How To Use the Storage Access API" here: https://webkit.org/blog/11545/updates-to-the-storage-access-api/

Let us know if you still have problems.
Comment 2 Jason Wilson 2022-06-21 08:31:10 PDT
Thank you .  I've read the guide several times.  The problem is that safari throws an exception when requestStorageAccess is called but the error object thrown is undefined so it is impossible to troubleshoot.
Comment 3 John Wilander 2022-06-21 08:33:27 PDT
Does the console in Web Inspector say anything?

To help you, we need to decide what to call the site you are loading in the iframe and what to call the site where the iframe is embedded. It can be the real domain names of those sites but doesn't have to be as long as we can refer to them as distinctly.
Comment 4 Jason Wilson 2022-06-21 09:34:39 PDT
It logs the that it doesn't have access to storage and then catches an exception when requestStorageAccess is called, but says the err object is undefined.  Here's an image to the console:  https://imgur.com/a/23QEOLA
Comment 5 John Wilander 2022-06-22 16:14:33 PDT
Those are your own console messages. I’m looking for console messages from the browser.

Let’s also get the domains. See my request above.
Comment 6 Jason Wilson 2022-06-23 13:28:33 PDT
I'm not sure I understand. When I don't catch the exception, I get the message "Unhandled Promise Rejected: undefined".

If I catch the error and look at the error object, it is undefined.

We can call the embedded address start.flashparking.com and the host address portal.flashparking.com.
Comment 7 Jason Wilson 2022-06-27 05:26:30 PDT
This is a show stopper on a major authentication project.  Is there anything that you can suggest?
Comment 8 John Wilander 2022-06-27 06:54:23 PDT
(In reply to Jason Wilson from comment #6)
> I'm not sure I understand. When I don't catch the exception, I get the
> message "Unhandled Promise Rejected: undefined".
> 
> If I catch the error and look at the error object, it is undefined.
> 
> We can call the embedded address start.flashparking.com and the host address
> portal.flashparking.com.

I checked the Public Suffix List (https://publicsuffix.org/list/public_suffix_list.dat) and flashparking.com is not a public suffix. This means that start.flashparking.com and portal.flashparking.com are just cross-origin and not cross-site. You therefore don't have to call the Storage Access API because you already have cookie access on start.flashparking.com under portal.flashparking.com.

If these domains were just made up as an example, please provide the real domain names so that we can establish whether or not you need to call the Storage Access API. Thanks!
Comment 9 John Wilander 2022-06-27 06:57:45 PDT
(In reply to Jason Wilson from comment #6)
> I'm not sure I understand. When I don't catch the exception, I get the
> message "Unhandled Promise Rejected: undefined".

There's a difference between what your JavaScript code sees through objects and what you as a developer can see in Web Inspector's console. There are certain warnings and errors that should not be exposed to code since they can be user against the user. Take for instance the reason for why there is no cookie access. If the code gets to know that the user explicitly refused to provide cookie access, that can potentially be used to pressure the user. This has already happened so it's a known problem. Therefore, look in Web Inspector's console for any additional information.
Comment 10 Jason Wilson 2022-06-27 07:53:35 PDT
I'm not seeing anything of note in the web inspector except what I've mentioned.  I should also mention that it never prompts the user to allow storage access.  It just errors when requestStorageAccess is called with no feedback.  The mac I'm testing on is not my development system, but here's a picture of the entire console:  https://imgur.com/a/xvsQpDh
Comment 11 Jason Wilson 2022-06-27 07:57:48 PDT
Here's an image of the actual code called by the page hosted in the iframe.  Line 20 is where the exception occurs: https://imgur.com/a/NC39tnR
Comment 12 John Wilander 2022-06-27 16:44:07 PDT
We have still not established that you need to use the Storage Access API at all since the domains you provided are same-site.

(In reply to Jason Wilson from comment #11)
> Here's an image of the actual code called by the page hosted in the iframe. 
> Line 20 is where the exception occurs: https://imgur.com/a/NC39tnR

You should not call hasStorageAccess() upon a user gesture.
You can only successfully call requestStorageAccess() upon a user gesture.

From the guide I linked to above:

Make your cross-site iframe call document.hasStorageAccess() as soon as it’s rendered to check your status. Note: Don’t call this function upon a user gesture since it’s asynchronous and will consume the gesture. Once the user gesture is consumed, making a subsequent call to document.requestStorageAccess() will fail because it’s not called when processing a user gesture.

If document.hasStorageAccess() returns false, your iframe doesn’t have storage access. Now set an event handler on elements that represent UI which requires storage access and make the event handler call document.requestStorageAccess() on a tap or click. This is the API that requires a user gesture.
Comment 13 Jason Wilson 2022-06-28 04:41:49 PDT
Sorry, the domains are different. For the test app the domains are the following:
- 3rd Party Page: https://sp-1729-component-api.d2vtqpn78oqsy5.amplifyapp.com/
- 1st Party Page: http://unifiedloginsample.azurewebsites.net/

requestStorageAccess is being called in a button click handler and the button resides in the 3rd party page.

I'll go ahead and try removing the hasStorageAccess from the handler.  I am calling both there.
Comment 14 Jason Wilson 2022-06-28 05:22:28 PDT
Removing the call to hasStorageAccess from the event handler did the trick.  I am pretty excited.  Thanks for your help.
Comment 15 Jason Wilson 2022-06-28 06:31:21 PDT
Another question -- for testing purposes, is it possible to remove the storage access once granted?
Comment 16 John Wilander 2022-06-28 06:43:49 PDT
(In reply to Jason Wilson from comment #15)
> Another question -- for testing purposes, is it possible to remove the
> storage access once granted?

There’s no programmatic way to do it since it’s a user decision to grant access. However, the access can age out if the user stops interacting with the site or the embedded content.
Comment 17 John Wilander 2022-06-28 06:44:45 PDT
Glad we could make this work for you!
Comment 18 Jason Wilson 2022-06-28 13:42:49 PDT
Is there a way for me to do it as a user, waiting for it age out definitely will slow down testing
Comment 19 John Wilander 2022-06-28 15:42:09 PDT
(In reply to Jason Wilson from comment #18)
> Is there a way for me to do it as a user, waiting for it age out definitely
> will slow down testing

You can test in Private Mode which forgets everything at browser restart. Just make sure you do the whole flow in the same tab since Private Mode is separated by tab.

Or you can delete your browsing history. If you don’t want to clear your own, real browsing history, you can use a separate Safari instance under a different macOS account or Safari Technology Preview.
Comment 20 Jason Wilson 2022-06-30 12:56:34 PDT
I thought it was supposed to remember that page in the IFrame has storage access, but I having to request each time I reload the page.

Again this works just fine in Firefox which also has an implementation of the Storage Access API.

Jason
Comment 21 Jason Wilson 2022-06-30 13:01:11 PDT
Also if the url changes at all.  I have to request storage access -- even if the change is just a change in the query string params
Comment 22 John Wilander 2022-06-30 15:29:23 PDT
(In reply to Jason Wilson from comment #20)
> I thought it was supposed to remember that page in the IFrame has storage
> access, but I having to request each time I reload the page.
> 
> Again this works just fine in Firefox which also has an implementation of
> the Storage Access API.
> 
> Jason

Yes, WebKit uses per-page scope whereas Gecko has a more relaxed model. Both are allowed and have been discussed extensively. WebKit’s position is that just because the user allows a third party to identify them on one page doesn’t mean they allow it for all pages on that site.

As an example, imagine the user wants to comment on a story on news.example with their social.example account. That does not imply that the user henceforth wants social.example to identify them on all pages on news.example and be able to track all the news they read there.
Comment 23 Jason Wilson 2022-07-01 05:27:48 PDT
Unfortunately it makes it useless for our cross-domain authentication.  Is there something in the proposed standard that allows for sites to opt out of the ITP if they control both domains -- perhaps allow iframes sandboxed with allow-same-origin to access 1st party storage. 

Our company has grown significantly in the last 2 years through acquisition and right now consolidating under a single domain isn't an option.  Cross-domain authentication is an intermediate step for us while we implement other auth schemes that don't use 3rd party cookies, but it's a necessary step.
Comment 24 John Wilander 2022-07-01 06:45:44 PDT
(In reply to Jason Wilson from comment #23)
> Unfortunately it makes it useless for our cross-domain authentication.  Is
> there something in the proposed standard that allows for sites to opt out of
> the ITP if they control both domains -- perhaps allow iframes sandboxed with
> allow-same-origin to access 1st party storage. 

There is no such affordance since there is no trustworthy way for a browser to know that two domains belong to the same company.

> Our company has grown significantly in the last 2 years through acquisition
> and right now consolidating under a single domain isn't an option. 
> Cross-domain authentication is an intermediate step for us while we
> implement other auth schemes that don't use 3rd party cookies, but it's a
> necessary step.

A major challenge here, which has been discussed at length in web standards, is that users have no reasonable way of knowing that two domains belong to the same company. Users also don’t expect a login on one site to invisibly log them in on another.

We typically advice developers to establish login tokens on the first party website where the user interacts instead of trying to maintain it as third-party. You make a transaction between the domains and then keep the login state as first party website.