Using Route Guards, Actions and Web Components
Tags: litelement, webcomponents, typescript, webdev
Published: 2020-12-12 02:25:46

Imagine you are building a Single Page Application using Web Components only. You have already defined a set of views, configured the routes, and you're handling the authentication very well. 

Suddenly, you understand that you must manage authorizations to decide to render a view or not. For example, you may need to manage the `/admin` and `/analytics` route for authorized users only.

In a previous post, I explained how to define rules to avoid [loading pages for restricted users](https://labs.thisdot.co/blog/navigation-lifecycle-using-vaadin-router-litelement-and-typescript) using the Navigation Lifecycle functions from [Vaadin Router](https://vaadin.com/router). In fact, a solution, based on the use of these functions, works very well for simple and isolated views.

However, what happens when you want to manage this type of authorization along a hierarchy of routes, or several routes at the same time? Let’s see how we can practically handle these types of scenarios using our [LitElement Website](https://github.com/luixaviles/litelement-website) based on TypeScript.

### Route Guards

Route Guards are also known as _Navigation Guards_, and they are implemented in most popular frameworks like [Angular](https://angular.io/api/router/CanActivate), [Vue](https://router.vuejs.org/guide/advanced/navigation-guards.html), and others. Let's take a look at the following image:

![auth-guard](//images.ctfassets.net/zojzzdop0fzx/4jzuTFDySpNQwZzf4hg4H9/8fe51afdddc2b96cf0bdf16258cdbb4e/auth-guard.jpg)

As you can see, there is an internal logic in your router configuration. This implementation can decide to proceed with rendering the associated view, or perform a redirection, by looking at an _authorization_ object, or any other state.

### Using Custom Route Actions
As a reminder, our [LitElement-based website](https://github.com/luixaviles/litelement-website) is based on TypeScript and the [Vaadin Router](https://vaadin.com/router) for routing management.

Turns out that _Vaadin Router_ allows [Custom Route Actions](https://vaadin.github.io/vaadin-router/vaadin-router/demo/#vaadin-router-route-actions-demos) configuration as a feature for advanced use cases.

This routing library provides a flexible API to customize the default route resolution rule. Each route may have an `action` property, which defines an additional behavior about the route resolution:

```ts
const routes: Route[] = [
  {
    path: '/',
    component: 'lit-component',
    action: async () => {
      ...
    },
  }
]
```

This action function can receive a `context` and `commands` as parameters:

```ts
import { Commands, Context } from '@vaadin/router';

const routes: Route[] = [
  {
    path: '/',
    component: 'lit-component',
    action: async (context: Context, commands: Commands) => {
      ...
    },
  }
]
```

Let's describe the `context` parameter and its properties:

| Property   |      Type      |  Description |
|----------|:-------------:|:------|
| `context.pathname` |  `string` | The pathname being resolved |
| `context.search` |  `string` | The search query string |
| `context.hash` |  `string` | The hash string |
| `context.params` |  `IndexedParams` | The route parameters object |
| `context.route` |  `Route` | The route that is currently being rendered |
| `context.next()` |  `function` | Function asynchronously getting the next route contents. Result: `Promise<ActionResult>` |

On other hand, the `commands` parameter contains a helper object with the following methods:

| Action   |      Result Type      |  Description |
|----------|:-------------:|:------|
| `commands.redirect('/path')` |  `RedirectResult` | Create and return a redirect command |
| `commands.prevent()` |  `PreventResult` | Create and return a prevent command |
| `commands.component('tag-name')` |  `ComponentResult` | Create and return a new `HTMLElement` that will be rendered into the router outlet |

### Implementing Route Guards
Before implementing the route guards, let's define the following structure:

```text
|- src/
    |- about/
    |- blog/
    |- admin/
    |- analytics/
    |- shared/
        |- auth/
```

Now we can consider two approaches for its implementation:

#### A class-based solution
If you have an Object-Oriented(OO) background like me, you can think of implementing a class model that can be easy to extend and maintain in the future.

Let's start with the `AuthorizationService` class definition. Create the following file `shared/auth/authorization-service.ts`, and add these lines of code:

```ts
// authorization-service.ts

export class AuthorizationService {
  private readonly key = 'key'; // Identifier for your key/token

  public isAuthorized(): Promise<boolean> {
    const token = this.getToken();
    return new Promise((resolve, reject) => {
      resolve(token !== null); // try using resolve(true) for testing
    });
  }

  setToken(token: string): void {
    localStorage.setItem(this.key, token);
  }

  getToken(): string | null {
    return localStorage.getItem(this.key);
  }
}
```

As you can see, this class is ready to instantiate objects, and you just need to call the `isAuthorized()` function to know if that given user should access a route path or not. Since this can be an asynchronous operation, the function signature is ready to return a `Promise`.

The `setToken` and `getToken` functions allow storing a _token_ value for a given user using [Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Of course, this is a simple way to handle it. 

You may consider other alternatives to store **temporary values** like [Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), or even [Session Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). Remember, it's always good to weigh the pros and cons of every option.

Try using `resolve(true)` on your tests if you're not adding a token through this file service.

Next, let's create the `AuthGuard` class in a `shared/auth/auth-guard.ts` file as follows:

```ts
// auth-guard.ts

import { Commands, Context, RedirectResult } from '@vaadin/router';
import { AuthorizationService } from './authorization-service';
import { PageEnabled } from './page-enabled';

export class AuthGuard implements PageEnabled {

  private authService: AuthorizationService;

  constructor() {
    this.authService = new AuthorizationService();
  }

  public async pageEnabled(context: Context, commands: Commands, pathRedirect?: string): Promise<RedirectResult | undefined> {
    const isAuthenticated = await this.authService.isAuthorized();

    if(!isAuthenticated) {
      console.warn('User not authorized', context.pathname);
     return commands.redirect(pathRedirect? pathRedirect: '/');
    }

    return undefined;
  }
}
```

This `AuthGuard` implementation expects to create an internal instance from `AuthorizationService` to validate the access through the `pageEnabled` function.

Also, this class needs to implement the `PageEnabled` interface defined into `/shared/auth/page-enabled.ts`:

```ts
//page-enabled.ts

import { Commands, Context, RedirectResult } from '@vaadin/router';

export interface PageEnabled {
  pageEnabled(
    context: Context,
    commands: Commands,
    pathRedirect?: string
  ): Promise<RedirectResult | undefined>;
}
```

This interface can act as a _contract_ for every _Auth Guard_ added to the routing configurations.

Finally, let's add the route configurations for the Analytics page:

```ts
// index.ts
import { AuthGuard } from './shared/auth/auth-guard';

const routes: Route[] = [
  {
    path: 'analytics',
    component: 'lit-analytics',
    action: async (context: Context, commands: Commands) => {
      return await new AuthGuard().pageEnabled(context, commands, '/blog');
    },
    children: [
      {
        path: '/', // Default component view for /analytics route
        component: "lit-analytics-home",
        action: async () => {
          await import('./analytics/analytics-home');
        },
      },
      {
        path: ':period', // /analytics/day, /analytics/week, etc
        component: 'lit-analytics-period',
        action: async () => {
          await import('./analytics/analytics-period');
        },
      },
    ]
  },
];
```

If you pay attention to the `action` parameter, the function will create a new `AuthGuard` instance, and it will verify if the current user can access the `/analytics` route, and its children.

#### A function-based solution
In case you're using a JavaScript approach with no classes, or want to add a single function to implement an _Auth Guard_, then you can create it as follows:

```ts
// auth-guard.ts

export async function authGuard(context: Context, commands: Commands) {
  const isAuthenticated = await new AuthorizationService().isAuthorized();

    if(!isAuthenticated) {
      console.warn('User not authorized', context.pathname);
     return commands.redirect('/');
    }

    return undefined;
}
```

It differs from the previous example in that this function does not support the additional parameter configuring the redirection path. However, this function would be easier to use in the route configuration:

```ts
// index.ts
import { authGuard } from './shared/auth/auth-guard';

const routes: Route[] = [
  {
    path: 'analytics',
    component: 'lit-analytics',
    action: authGuard, // authGuard function reference
    children: [
      ...
    ]
  },
];
```

Once finished, try to load either `http://localhost:8000/analytics` or `http://localhost:8000/analytics/month` to see the next result when you're not authorized:

![screenshot-not-authorized](//images.ctfassets.net/zojzzdop0fzx/7GuYl3BhBmEr1dj3zxzebC/501e580ddfbf92f34aa9b2f2a8d1dccf/screenshot-not-authorized.png)

In this case, the user got redirected to `/blog` path.

Otherwise, you may be able to access those routes(Remember to change `resolve(token !== null)` by `resolve(true)` on `authorization-service.ts` file to verify).

Now you're ready to use an _Auth Guard_ for a single or multiple routes!

### Source Code Project

Find the complete project in this GitHub repository: [https://github.com/luixaviles/litelement-website](https://github.com/luixaviles/litelement-website). Do not forget to give it a star ⭐️ and play around with the code.

You can follow me on [Twitter](https://twitter.com/luixaviles) and [GitHub](https://github.com/luixaviles) to see more about my work.

---

*This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](https://www.thisdotlabs.com).*

*This Dot Media is focused on creating an inclusive and educational web for all.  We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co).*