An approach to client-side authorization in VueJS
Tags: vue, javascript
Published: 2020-06-13 03:43:46

With the widespread adoption of frontend frameworks, it has become common to see apps built as standalone client-heavy apps that communicate with a backend API, with this paradigm comes a number of challenges and interesting ways to solve them. One of these challenges is authorization, in this post I will share some ideas on how to approach this as well a plug an open-source library I put together to facilitate this approach, please share your thoughts with me on the merits and demerits of this approach.

## Goals
According to the Vue.js docs:

>At the core of Vue.js is a system that enables us to declaratively render data to the DOM using straightforward template syntax:

It also offers imperative escape hatches like `watch` and lifecycle methods and the most touted point in favour of Vue.js is its approachability.

So we want a solution that is
 - Declarative and composable,
 - Offers imperative escape hatches and,
 - Has an approachable and easy to reason about API.

## I promise we will get to the code soon.  

## The API

To start with, authorization involves granting or denying access to a resource and technically it involves identifying what resources the user should have access to, from these I find that the inputs to the system are `requiredPermissions` and`userPermissions` also the output is a boolean `true` or `false`. It's also possible we want more fine-grained control and so we may allow or disallow access if the `userPermissions` include `all` of the `requiredPermissions` or in other cases, it's okay if they have `some` of the `requiredPermissions`.
so we have identified a third input - a boolean control `all`.

At first I wanted to use VueJS directive like
```vue
<SomeComponent v-guard="['is-admin', 'is-shopowner']"/>
```
But after a few hours of failing to get it to work I stumbled on a thread on Vue Forum where its was suggested that using a directive was ill advised. so I tried a functional component instead.

```vue
<v-guard :permissions="['is-admin', 'is-shopowner']">
  <SomeComponent/>
</v-guard>
```
This meets our goal of a declarative API.
For imperative operations like making requests in our methods or providing feedback if a user does not have permission, we can inject methods like 
```vue
<script>
export default {
  methods:{
   makeSomeRequest(somedata){
    // there is also this.$permitsAll()
    If(this.$permitsAny(['add-post', 'update-post']){
       // make the request here
      }else{
       //you need to do pay us in order to do this.
      }
    }
  }
}
</script>

<template>
<!-- in templates -->
 <button :disabled="!$permitsAny(['add-post', 'update-post'])>Update this post</button>
</template>
```
The `v-guard` component will not cover disabling it's children/slots as it works on the Virtual DOM layer and completely avoids rendering it's children.

Finally, for `routes` we could still use the imperative `beforeEnter` hook and check however we can take this one level up by doing so in library code so the userland API is just to mark the routes with the required permissions like this.

```js
 const routes = [
 {
  path: ':id/edit',
  name: 'EditPost',
  meta: { 
    guard: {
      permissions: ['edit-posts', 'manage-posts'],
      all: true
    },
 }
]

```
All that remains now is to provide a way for the developer to provide the plugin with the user's permission. For this, we can just require them to provide an option on the root of their component tree, this could be a function or just an array let's call it `permissions` (I am terrible at naming things 🤷🏾‍♂️️)  If it's a function it should synchronously return an array of the user's permissions

## Finally, the code.
We break the problem into bits and assemble the solutions in a plugin.

### Setup
When installing the plugin we would call the permissions function option the developer has implemented in their root component attach it to the Vue prototype so it can be called from any component as a normal member. We can do this in the `beforeCreate` lifecycle this is how Vuex makes `$store` available in every component.

```js
Vue.mixin({
    beforeCreate: permissionsInit
  });

  function permissionsInit(this: Vue) {

    let permFn = getPropFromSelfOrAcenstor("permissions", this.$options);
    if (!permFn) {
       console.error(
          `[v-guard]`,
          `To use this plugin please add a "permissions" synchronuous function or object option as high up your component tree as possible.`
        );
      return;
    }
    Vue.prototype.$getPermissions =
      typeof permFn === "function" ? permFn.bind(this) : () => permFn;
 
    let perms = typeof permFn === "function" ? permFn.call(self) : permFn;
    
   Vue.prototype.$permitsAll = function permitsAll(permissions: Permissions) {
      //we will discuss the implementation of isPermitted shortly
      return isPermitted(perms, permissions, true);
    };
    Vue.prototype.$permitsAny = function permitsAll(permissions: Permissions) {
      return isPermitted(perms, permissions, false);
    };
  }

//helper function to recursively get a property from a component or it's parent.
function getPropFromSelfOrAcenstor(
  prop: string,
  config: ComponentOptions
): Function | null {
  if (config[prop]) {
    return config[prop];
  }
  if (config.parent) {
    return getPropFromSelfOrAcenstor(prop, config.parent);
  }
  return null;
}
```

When the plugin is installed we call `permissionsInit` on the beforeCreate of every component, this function takes the component instance and gets the `permissions` option (the function or object the client code must implement) from the component or it's parent using a helper function `getPropsFromSelfOrAncestor` if this has not been implemented we stop processing and warn the user.

Now having the user's permissions we add the imperative parts of our API `$permitsAll` and `$permitsAny` this delegate to an `isPermitted` function which we would now show.

```js
function isPermitted(
  usersPermissions: Array<string>,
  permissions: Permissions, // Array | string
  all: boolean
) {
  if (!permissions || !usersPermissions) {
    throw new Error(`isPermitted called without required arguments`);
  }
  permissions = Array.isArray(permissions)
    ? permissions
    : permissions.trim().split(",");

  let intersection = permissions.reduce(
    (intersect: Array<string>, perm: string) => {
      if (
        !usersPermissions.map((s: string) => s.trim()).includes(perm.trim())
      ) {
        return intersect;
      }
      if (!intersect.includes(perm.trim())) {
        intersect.push(perm);
      }
      return intersect;
    },
    []
  );
  return all
    ? intersection.length >= permissions.length
    : intersection.length > 0;
}
```
This function takes the user's permissions and the required permissions and determines the common element (intersection) between these. it also takes a third control argument (boolean `all`). If all the required permissions are necessary (`all = true`) then the common elements array should have the same members as the user's permission, if however not all the required permissions are necessary, (`all = false`) we only need to have at least one common element. I know this may seem like too much but I find it's easier to reason about the function as a Set problem that way the mental model is clear.
We also account for passing a comma-separated string as the required permissions, this makes the library more flexible. Finally, there is a lot of trimming to deal with extraneous whitespace characters.

This function could use two major refactors 
 
  - Use a `Set` for the intersection, that way we don't need to check if it already contains the permission in we are looping over.

  - Memoize the function so we don't recalculate intersections for which we already know the outcome. this is useful when rendering a list of items that are guarded.

I would look into this for a patch to the library I wrote.


### V-guard component to conditionally render component trees.
For this, we will use a functional component as they are cheaper to render and we don't really need state so they are sufficient.

```js
Vue.component("v-guard", {
    functional: true,
    props: {
      permissions: {
        type: [Array, String],
        default: () => []
      },
      all: {
        type: Boolean,
        default: false
      }
    },
    render(h, { props, slots, parent }) {
      let { $getPermissions } = parent;
      if (!$getPermissions) {
        console.error(
          `[v-guard]`,
          `v-guard must be a descendant of a component with a "permissions" options`
        );
      }
      const { permissions, all } = props;

      if (
        isPermitted($getPermissions() || [], permissions as Permissions, all)
      ) {
        return slots().default;
      }
      return h();
    }
  });

``` 
Functional components in Vue.js have a `context` variable passed to their `render` function, this contains the among other things `props`, `slots` and `parent` which we need. from the `parent`, we can grab the `$getPermissions` which we injected during the plugin installation.

*Due to the nature of functional components, the `$getPermission` function is not injected into it as it's not an object instance, it's a function.* 

In the `render` function we call the `isPermitted` function with the user's permission which we now have access to by calling `$getPermissions` and the required permissions which have been passed as props to the `v-guard` component.

```vue
//permissions here are passed as props.
<v-guard :permissions="['is-admin', 'is-shopowner']">
  <SomeComponent/>
</v-guard>
```

### For routes

When installing the plugin the developer can pass as `router` option to the plugin, which is a VueRouter instance. (this would also require them to pass an `errorRoute` string which is the route to go to for unauthorized actions)

```js
function PermissionPlugin(
  Vue: VueConstructor,
  options: VueGuardOptions = {}
): void {
  if (options.router) {
    addRouterGuards(options.router, options.errorRoute);
  }
  Vue.component("v-guard", {
    functional: true,
    ...// we covered this already
  })

function addRouterGuards(router: VueRouter, errorRoute : string) {
    router.beforeResolve(
      (to: RouteRecord, from: RouteRecord, next: Function) => {
   
        const guard = to.meta && to.meta.guard;
        if (!guard) {
          return next();
        }
        const { $getPermissions } = Vue.prototype;
        if (!$getPermissions) {
          if (errorRoute) {
            return next(errorRoute);
          }
          throw new Error(`You need to pass an "errorRoute"  string option `);
        }

        const usersPermissions = $getPermissions();
        const {  permissions, all = true } = guard;

       if (!isPermitted(usersPermissions, permissions, all)) {
          return next(errorRoute);
        }
        return next();
      }
    );
  }
}

}
```

Here we use VueRouter's `beforeResolve` guard to check if the user is permitted to view the route in which case we proceed to the route, else we redirect them t the `errorRoute`.

To use the library now the developer would do something like

```js

//Permissions is the plugin, router is a VueRouter instance,
Vue.use(Permissions, { router, errorRoute: "/403" });

new Vue({
  router,
  permissions() {
    return this.$store.getters.userPermissions;
  },
  render: h => h(App),
 ...// other options
}).$mount('#app')

```


Please share your thoughts and suggestions. thanks.