7

Goal: To support dynamic loading of Javascript modules contingent on some security or defined user role requirement such that even if the name of the module is identified in dev tools, it cannot be successfully imported via the console.

enter image description here

enter image description here

enter image description here

A JavaScript module can be easily uploaded to a cloud storage service like Firebase (#AskFirebase) and the code can be conditionally retrieved using a Firebase Cloud Function firebase.functions().httpsCallable("ghost"); based on the presence of a custom claim or similar test.

export const ghost = functions.https.onCall(async (data, context) => {
  if (! context.auth.token.restrictedAccess === true) {
    throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
  }

  const storage = new Storage();
  const bucketName = 'bucket-name.appspot.com';
  const srcFilename = 'RestrictedChunk.chunk.js';

  // Downloads the file
  const response = await storage
    .bucket(bucketName)
    .file(srcFilename).download();
  const code = String.fromCharCode.apply(String, response[0])

  return {source: code};

})

In the end, what I want to do...

...is take a webpack'ed React component, put it in the cloud, conditionally download it to the client after a server-side security check, and import() it into the user's client environment and render it.

Storing the Javascript in the cloud and conditionally downloading to the client are easy. Once I have the webpack'ed code in the client, I can use Function(downloadedRestrictedComponent) to add it to the user's environment much as one would use import('./RestrictedComponent') but what I can't figure out is how to get the default export from the component so I can actually render the thing.

import(pathToComponent) returns the loaded module, and as far as I know there is no option to pass import() a string or a stream, just a path to the module. And Function(downloadedComponent) will add the downloaded code into the client environment but I don't know how to access the module's export(s) to render the dynamically loaded React components.

Is there any way to dynamically import a Javascript module from a downloaded stream?

Edit to add: Thanks for the reply. Not familiar with the nuances of Blobs and URL.createObjectURL. Any idea why this would be not found?

const ghost = firebase.functions().httpsCallable("ghost");

const LoadableRestricted = Loadable({
  //  loader: () => import(/* webpackChunkName: "Restricted" */ "./Restricted"),
  loader: async () => {
    const ghostContents = await ghost();
    console.log(ghostContents);
    const myBlob = new Blob([ghostContents.data.source], {
      type: "application/javascript"
    });
    console.log(myBlob);
    const myURL = URL.createObjectURL(myBlob);
    console.log(myURL);
    return import(myURL);
  },
  render(loaded, props) {
    console.log(loaded);
    let Component = loaded.Restricted;
    return <Component {...props} />;
  },
  loading: Loading,
  delay: 2000
});

enter image description here

kjhughes
  • 106,133
  • 27
  • 181
  • 240
shaz
  • 2,317
  • 4
  • 27
  • 37
  • Did you ever find a complete solution that you liked? – kjhughes Oct 31 '19 at 14:47
  • Shaz or anyone else: Have a complete solution to this problem? **I'd like to assign the +500 bounty, but noone has stepped up, and it expires outside of my control tomorrow.** – kjhughes Nov 04 '19 at 16:12
  • @kjhughes If I understand this correctly, you download a file into a directory `./RestrictedComponent.js` and if the user has been authenticated, import that component? Is so, have you considered [React.lazy](https://reactjs.org/docs/code-splitting.html#reactlazy)? – radihuq Nov 05 '19 at 19:59
  • @radihuq: `React.lazy` may help with the dynamic import aspect (thank you) but doesn't itself prevent unauthenticated users from retrieving the restricted JavaScript from the server by GET'ing from the endpoint apart from the client UI check. – kjhughes Nov 05 '19 at 22:16
  • @kjhughes ah okay I see. Maybe server side rendering is the solution, or [something like this](https://reactjs.org/docs/react-dom-server.html) but I don't have much experience with that. Good luck ! – radihuq Nov 06 '19 at 01:03
  • @radihuq: Server side rendering isn't an option as the module is needed locally for more than just page rendering. Thanks, though. – kjhughes Nov 06 '19 at 01:17
  • Did you ever overcome the Error: Cannot find module 'blob: ? – Dennis Bauszus Nov 28 '19 at 11:58

2 Answers2

4

Read the contents of the module file/stream into a BLOB. The use URL.createObjectURL() to create your dynamic URL to the BLOB. Now use import as you suggested above:

import(myBlobURL).then(module=>{/*doSomethingWithModule*/});
Randy Casburn
  • 13,840
  • 1
  • 16
  • 31
  • Thanks @Randy Casburn, edited with "Cannot find module..." error above. – shaz Oct 19 '18 at 17:52
  • never knew about this blob thing; so one (quite obvious) thing to consider is that the js code fed there would be compiled js. But webpack not only compiles JS but also replaces imports with its own import logic. Imagine this file you import is code-split and can yet trigger other imports... – wkrueger Oct 30 '19 at 02:28
  • 1
    @wkrueger: Note that OP's module is post-Webpack. Also note my interest in a complete working code sample is not limited to Randy's blob-based approach. Thanks. – kjhughes Oct 30 '19 at 12:13
-2

You can try using React.lazy:

import React, {lazy, Suspense} from 'react';

const Example = () => {

    const [userAuthenticated, setUserAuthenticated] = useState(true);

    if (userAthenticated) {

        const RestrictedComponent = lazy(() => import('./RestrictedComponent'));

        return (
            <div>
                <Suspense fallback={<div><p>Loading...</p></div>}>
                    <RestrictedComponent />
                </Suspense>
            </div>
        )
    }

    return (
        <div>
            <h1>404</h1>
            <p>Restricted</p>
        </div>
    );

}

export default Example;
radihuq
  • 999
  • 10
  • 13
  • The question is not related to *React* and is about importing *streams* and *blobs* instead of urls – vsync May 26 '21 at 12:20