Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add authorization header when runtime import webpack chunks of Vue components

The purpose of this task is to make it impossible to download the Vue-component package (*.js file) knowing the address of the component, but not having an access token.

I'm developing an access control system and a user interface in which the set of available components depends on the user's access level.

The system uses the JSON API and JWT authorization. For this, Axios is used on the client side. To build the application, we use Webpack 4, to load the components, we use the vue-loader.

After the user is authorized, the application requests an array of available routes and metadata from the server, then a dynamically constructed menu and routes are added to the VueRouter object.

Below I gave a simplified code.

            import axios from 'axios'
            import router from 'router'

            let API = axios.create({
              baseURL: '/api/v1/',
              headers: {
                Authorization: 'Bearer mySecretToken12345'
              }
            })

            let buildRoutesRecursive = jsonRoutes => {
              let routes = []
              jsonRoutes.forEach(r => {
                let path = r.path.slice(1)
                let route = {
                  path: r.path,
                  component: () => import(/* webpackChunkName: "restricted/[request]" */ 'views/restricted/' + path)
                  //example path: 'dashboard/users.vue', 'dashboard/reports.vue', etc...
                }
                if (r.children)
                  route.children = buildRoutesRecursive(r.children)
                routes.push(route)
              })
              return routes
            }

            API.get('user/routes').then(
              response => {

                /*
                  response.data = 
                        [{
                      "path": "/dashboard",
                      "icon": "fas fa-sliders-h",
                              "children": [{
                        "path": "/dashboard/users",
                        "icon": "fa fa-users",
                                }, {
                        "path": "/dashboard/reports",
                        "icon": "fa fa-indent"
                                }
                            ]
                        }
                    ]
                */

                let vueRoutes = buildRoutesRecursive(response.data)
                router.addRoutes(vueRoutes)   
              },
              error => console.log(error)
            )

The problem I'm having is because Webpack loads the components, by adding the 'script' element, and not through the AJAX request. Therefore, I do not know how to add an authorization header to this download. As a result, any user who does not have a token can download the code of the private component by simply inserting his address into the navigation bar of the browser.

import dashboard-reports.vue

Ideally, I would like to know how to import a vue component using Axios.

import using ajax

Or, how to add an authorization header to an HTTP request.

Auth header in script request

like image 958
Nikolay D. Avatar asked Apr 09 '18 11:04

Nikolay D.


1 Answers

I needed something similar and came up with the following solution. First, we introduce a webpack plugin that gives us access to the script element before it's added to the DOM. Then we can munge the element to use fetch() to get the script source, and you can craft the fetch as needed (e.g. add request headers).

In webpack.config.js:

/*
 * This plugin will call dynamicImportScriptHook() just before
 * the script element is added to the DOM. The script object is
 * passed to dynamicImportScriptHook(), and it should return
 * the script object or a replacement.
 */
class DynamicImportScriptHookPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "DynamicImportScriptHookPlugin", (compilation) =>
        compilation.mainTemplate.hooks.jsonpScript.tap(
          "DynamicImportScriptHookPlugin", (source) => [
            source,
            "if (typeof dynamicImportScriptHook === 'function') {",
            "  script = dynamicImportScriptHook(script);",
            "}"
          ].join("\n")
        )
    );
  }
}

/* now add the plugin to the existing config: */
module.exports = {
   ...
   plugins: [
     new DynamicImportScriptHookPlugin()
   ]
}

Now, somewhere convenient in your application js:

/*
 * With the above plugin, this function will get called just
 * before the script element is added to the DOM. It is passed
 * the script element object and should return either the same
 * script element object or a replacement (which is what we do
 * here).
 */
window.dynamicImportScriptHook = (script) => {
  const {onerror, onload} = script;
  var emptyScript = document.createElement('script');
  /*
   * Here is the fetch(). You can control the fetch as needed,
   * add request headers, etc. We wrap webpack's original
   * onerror and onload handlers so that we can clean up the
   * object URL.
   *
   * Note that you'll probably want to handle errors from fetch()
   * in some way (invoke webpack's onerror or some such).
   */
  fetch(script.src)
    .then(response => response.blob())
    .then(blob => {
      script.src = URL.createObjectURL(blob);
      script.onerror = (event) => {
        URL.revokeObjectURL(script.src);
        onerror(event);
      };
      script.onload = (event) => {
        URL.revokeObjectURL(script.src);
        onload(event);
      };
      emptyScript.remove();
      document.head.appendChild(script);
    });
  /* Here we return an empty script element back to webpack.
   * webpack will add this to document.head immediately.  We
   * can't let webpack add the real script object because the
   * fetch isn't done yet. We add it ourselves above after
   * the fetch is done.
   */
  return emptyScript;
};
like image 157
sspiff Avatar answered Sep 26 '22 15:09

sspiff