Search code examples
javascriptfirefoxcookiesiframestorage-access-api

Firefox Storage Access API denies local storage after successful requestStorageAccess() call


I want to test the new Firefox Storage Access API to allow 1st party storage (cookie, local storage, indexeddb, ...) to an iframe of a different domain (but still under my control).

Parent Markup / code

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Parent Domain</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jschannel/1.0.0-git-commit1-8c4f7eb/jschannel.min.js"></script>
    </head>
    <body>
        <div>
            Cookies: <ul class="cookie-data"></ul>
        </div>
        <iframe 
            id="rpc-gateway"
            src="http://child.local:8080/iframe-firefox.html"
            sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin"></iframe>
        <script type="text/javascript">            
            var chan = Channel.build({
                window: document.getElementById("rpc-gateway").contentWindow,
                origin: "*",
                scope: "testScope"
            });
        </script>
    </body>
</html>

Child Iframe Markup / code

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Child Domain</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jschannel/1.0.0-git-commit1-8c4f7eb/jschannel.min.js"></script>
    </head>
    <body>
        <button onClick="onLoginClick()">Login</button>
        <script type="text/javascript">
            var chan = Channel.build({
                window: window.parent,
                origin: "*",
                scope: "testScope"
            });

            let onLoginClick = function(trans, params) {
                document.hasStorageAccess().then(hasAccess => {
                    if (!hasAccess) {
                        console.log("no access - requesting access");
                        return document.requestStorageAccess();
                    }
                }).then(_ => {
                    document.hasStorageAccess().then(hasAccess => {
                        console.log("hasAccess:", hasAccess);
                        window.localStorage.setItem('foo', 'bar');
                    })
                }).catch((err) => {
                    console.log("hasStorageAccess() failed", err);
                });
            };
        </script>
    </body>
</html>

When clicking on the "Login" button from the Child Iframe, the following log output is generated:

no access - requesting access      # iframe-firefox.html:22:25
hasAccess: true      # iframe-firefox.html:27:25
Request to access cookie or storage on “http://child.local:8080/iframe-firefox.html” was blocked because we are blocking all third-party storage access requests and content blocking is enabled.      # iframe-firefox.html:28:24

The visible conclusion is:

  • The promise document.hasStorageAccess() resolves
  • The hasAccess parameter is initially 'false'
  • The promise of document.requestStorageAccess() is returned and resolves
  • The 2nd promise document.hasStorageAccess() resolves
  • The hasAccess parameter is now 'true'
  • nevertheless, simple storage access to local storage is not possible.

What do I do wrong?

More Info's:

  • Firefox Developer Edition Version 65.0b9
  • Content Blocking Setting: Content Blocking Setting

Solution

  • This seems to be a bug in the version of Firefox you're using. I set up a test locally of what you have and in Firefox 69.0.1 (64 bit), I get no error and the value is stored to local storage. When I took the sandbox flag allow-storage-access-by-user-activation out of the parent iframe, the child failed to get permission for local storage, so that confirms that my setup was actually working properly. Here's what I did:

    Created a Node.js/Express server for the parent:

    const express = require('express');
    const cors = require('cors');
    const path = require('path');
    const server = express();
    
    server.use(cors());
    server.use(express.static(path.resolve('./public')));
    
    server.listen(8080, function() {
      console.log('listening on *:8080');
    });
    

    Created a Node.js/Express server for the child (with different port to trigger same origin policy):

    const express = require('express');
    const cors = require('cors');
    const path = require('path');
    const server = express();
    
    server.use(cors());
    server.use(express.static(path.resolve('./public')));
    
    server.listen(8081, function() {
      console.log('listening on *:8081');
    });
    

    Created an index.html for the parent (pretty much the same as yours):

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8" />
            <title>Parent Domain</title>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/jschannel/1.0.0-git-commit1-8c4f7eb/jschannel.min.js"></script>
        </head>
        <body>
            <div>
                Cookies: <ul class="cookie-data"></ul>
            </div>
            <iframe
                id="rpc-gateway"
                src="http://127.0.0.1:8081/iframe-firefox.html"
                sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin"></iframe>
            <script type="text/javascript">
                var chan = Channel.build({
                    window: document.getElementById("rpc-gateway").contentWindow,
                    origin: "*",
                    scope: "testScope"
                });
    
                // Added this to try out the JSChannel
                chan.call({
                    method: "reverse",
                    params: "hello world!",
                    success: function(v) {
                        console.log(v);
                    }
                });
    
            </script>
        </body>
    </html>
    

    And created iframe-firefox.html for the child:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8" />
            <title>Child Domain</title>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.0/js.cookie.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/jschannel/1.0.0-git-commit1-8c4f7eb/jschannel.min.js"></script>
        </head>
        <body>
            <button onClick="onLoginClick()">Login</button>
            <script type="text/javascript">
                var chan = Channel.build({
                    window: window.parent,
                    origin: "*",
                    scope: "testScope"
                });
    
                // Other end of the JSChannel call
                chan.bind("reverse", function(trans, s) {
                    return s.split("").reverse().join("");
                });
    
                let onLoginClick = function(trans, params) {
                    document.hasStorageAccess().then(hasAccess => {
                        if (!hasAccess) {
                            console.log("no access - requesting access");
                            return document.requestStorageAccess();
                        }
                    }).then(_ => {
                        document.hasStorageAccess().then(hasAccess => {
                            console.log("hasAccess:", hasAccess);
                            window.localStorage.setItem('foo', 'bar');
                        })
                    }).catch((err) => {
                        console.log("hasStorageAccess() failed", err);
                    });
                };
            </script>
        </body>
    </html>
    

    And everything worked as expected... So I'm feeling pretty sure that the issue is with the specific version of Firefox Developer Edition that you're using.

    Also, here's a link to a zip of my setup if you want to give it a try on your end and see if this works differently than what you have: server.zip

    Let me know if there's anything else I can do to help.