Skip to content

Commit

Permalink
docs: add guideline doc for authorise component
Browse files Browse the repository at this point in the history
Add documentation specifying guidelines on creating an authorisation component

re loopbackio#538
  • Loading branch information
samarpanB committed Apr 12, 2019
1 parent 47350c3 commit 07dfb7b
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 0 deletions.
330 changes: 330 additions & 0 deletions docs/site/Loopback-component-authorisation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
---
lang: en
title: 'Authorisation'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Loopback-component-authorisation.html
---

## Overview

In every web application, we need to have a way to identify
access rights of a user for any resource, which is known as **Authorisation**.
This is a minimalistic guide for creating such an implementation
using Loopback component.
This can be part of your main REST Application project or
can be created as a Loopback extension for reuse in multiple projects.
Latter is the better option for obvious reasons - reusability.

## The requirement

1. Every protected API end point needs to be restricted by specific permissions.
2. API allows access only if logged in user has permission as per end point restrictions.
3. API throws **403 Forbidden** error if logged in user do not have sufficient permissions.
4. Publicly accessible APIs must be accessible regardless of user permissions.
5. Every user has a set of permissions. These permissions may be associated via role attached to the user or directly to the user.

## The implementation

First, let's define the types needed for this.

{% include code-caption.html content="/src/authorisation/types.ts" %}

```ts
/**
* Authorise action method interface
*/
export interface AuthoriseFn {
(userPermissions?: string[]): Promise<boolean>;
}

/**
* Authorisation metadata interface for the method decorator
*/
export interface AuthorisationMetadata {
permissions: string[];
}
```

We define two interfaces.

- ***AuthoriseFn*** - This is going to be the interface for authorisation action business logic.
- ***AuthorisationMetadata*** - This interface represents the information to be passed via decorator for each individual controller method.

Next, we create the binding keys for each type and accessor key for method decorator.

{% include code-caption.html content="/src/authorisation/keys.ts" %}

```ts
import {BindingKey} from '@loopback/context';
import {MetadataAccessor} from '@loopback/metadata';
import {AuthoriseFn, AuthorisationMetadata} from './types';

/**
* Binding keys used by this component.
*/
export namespace AuthorisatonBindings {
export const AUTHORISE_ACTION = BindingKey.create<AuthoriseFn>(
'userAuthorisation.actions.authorise',
);

export const METADATA = BindingKey.create<AuthorisationMetadata | undefined>(
'userAuthorisation.operationMetadata',
);
}

/**
* Metadata accessor key for authorise method decorator
*/
export const AUTHORISATION_METADATA_ACCESSOR = MetadataAccessor.create<
AuthorisationMetadata,
MethodDecorator
>('userAuthorisation.accessor.operationMetadata');
```

Now, we need to create two providers

- ***AuthorisationMetadataProvider*** - This will read the decorator metadata from the controller methods wherever the decorator is used.
- ***AuthoriseActionProvider*** - This holds the business logic for access validation of the user based upon access permissions allowed at method level via decorator metadata above.

{% include code-caption.html content="/src/authorisation/providers/authorisation-metadata.provider.ts" %}

```ts
import {
Constructor,
inject,
MetadataInspector,
Provider,
} from '@loopback/context';
import {CoreBindings} from '@loopback/core';

import {AUTHORISATION_METADATA_ACCESSOR} from '../keys';
import {AuthorisationMetadata} from '../types';

export class AuthorisationMetadataProvider
implements Provider<AuthorisationMetadata | undefined> {
constructor(
@inject(CoreBindings.CONTROLLER_CLASS)
private readonly controllerClass: Constructor<{}>,
@inject(CoreBindings.CONTROLLER_METHOD_NAME)
private readonly methodName: string,
) {}

value(): AuthorisationMetadata | undefined {
return getAuthoriseMetadata(this.controllerClass, this.methodName);
}
}

export function getAuthoriseMetadata(
controllerClass: Constructor<{}>,
methodName: string,
): AuthorisationMetadata | undefined {
return MetadataInspector.getMethodMetadata<AuthorisationMetadata>(
AUTHORISATION_METADATA_ACCESSOR,
controllerClass.prototype,
methodName,
);
}
```

{% include code-caption.html content="/src/authorisation/providers/authorisation-action.provider.ts" %}

```ts
import {Getter, inject, Provider} from '@loopback/context';

import {AuthorisatonBindings} from '../keys';
import {AuthorisationMetadata, AuthoriseFn} from '../types';

import {intersection} from 'lodash';

export class AuthoriseActionProvider implements Provider<AuthoriseFn> {
constructor(
@inject.getter(AuthorisatonBindings.METADATA)
private readonly getMetadata: Getter<AuthorisationMetadata>,
) {}

value(): AuthoriseFn {
return response => this.action(response);
}

async action(userPermissions: string[]): Promise<boolean> {
const metadata: AuthorisationMetadata = await this.getMetadata();
if (!metadata) {
return false;
} else if (metadata.permissions.indexOf('*') === 0) {
// Return immediately with true, if allowed to all
// This is for publicly open routes only
return true;
}

// Add your own business logic to fetch or
// manipulate with user permissions here

const permissionsToCheck = metadata.permissions;
return intersection(userPermissions, permissionsToCheck).length > 0;
}
}
```

Next, we need to expose these providers via Component to be bound to the context.

{% include code-caption.html content="/src/authorisation/component.ts" %}

```ts
import {Component, ProviderMap} from '@loopback/core';
import {AuthorisatonBindings} from './keys';
import {AuthoriseActionProvider} from './providers/authorisation-action.provider';
import {AuthorisationMetadataProvider} from './providers/authorisation-metadata.provider';

export class AuthorisationComponent implements Component {
providers?: ProviderMap;

constructor() {
this.providers = {
[AuthorisatonBindings.AUTHORISE_ACTION.key]: AuthoriseActionProvider,
[AuthorisatonBindings.METADATA.key]: AuthorisationMetadataProvider,
};
}
}
```

You can see that we have used the same binding keys which we created earlier.

Now, its time to create our method decorator function. Here it is. We will be using the same metadata accessor key which we created earlier and the metadata interface for accessing the data in decorator.

{% include code-caption.html content="/src/authorisation/decorators/authorise.decorator.ts" %}

```ts
import {MethodDecoratorFactory} from '@loopback/core';
import {AuthorisationMetadata} from '../types';
import {AUTHORISATION_METADATA_ACCESSOR} from '../keys';

export function authorise(permissions: string[]) {
return MethodDecoratorFactory.createDecorator<AuthorisationMetadata>(
AUTHORISATION_METADATA_ACCESSOR,
{
permissions: permissions || [],
},
);
}
```

For error handling keys, lets create an enum.

{% include code-caption.html content="/src/authorisation/error-keys.ts" %}

```ts
export const enum AuthoriseErrorKeys {
NotAllowedAccess = 'Not Allowed Access',
}

```

Finally, we put everything together in one index file.

{% include code-caption.html content="/src/authorisation/index.ts" %}

```ts
export * from './component';
export * from './types';
export * from './keys';
export * from './error-keys';
export * from './decorators/authorise.decorator';
export * from './providers/authorisation-metadata.provider';
export * from './providers/authorisation-action.provider';
```

That is all for the authorisation component. You can create all of the above into a loopback extension as well. Everything remains the same. Refer to the [extension generator](./Extension-generator.md) guide for creating an extension.

## Usage

In order to use the above component into our REST API application, we have a few more steps to go.

- Add component to application.

{% include code-caption.html content="/src/application.ts" %}

```ts
this.component(AuthenticationComponent);
```

- Add a step in custom sequence to check for authorisation whenever any end point is hit.

{% include code-caption.html content="/src/sequence.ts" %}

```ts
import {inject} from '@loopback/context';
import {
FindRoute,
InvokeMethod,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
HttpErrors,
} from '@loopback/rest';
import {
AuthorisatonBindings,
AuthoriseFn,
AuthoriseErrorKeys,
} from './authorisation';

const SequenceActions = RestBindings.SequenceActions;

export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthorisatonBindings.AUTHORISE_ACTION)
protected checkAuthorisation: AuthoriseFn,
) {}

async handle(context: RequestContext) {
try {
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
// Do authentication of the user and fetch user permissions below
const permissions: string[] = [];
// This is main line added to sequence
// where we are invoking the authorise action function to check for access
const isAccessAllowed: boolean = await this.checkAuthorisation(
permissions,
);
if (!isAccessAllowed) {
throw new HttpErrors.Forbidden(AuthoriseErrorKeys.NotAllowedAccess);
}
const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
this.reject(context, err);
}
}
}
```

Now we can add access permission keys to the controller methods using authorise decorator as below.

```ts
@authorise(['CanCreateRole'])
@post(rolesPath, {
responses: {
[STATUS_CODE.OK]: {
description: 'Role model instance',
content: {
[CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
},
},
},
})
async create(@requestBody() role: Role): Promise<Role> {
return await this.roleRepository.create(role);
}
```

This endpoint will only be accessible if logged in user has permission 'CanCreateRole'.
11 changes: 11 additions & 0 deletions docs/site/Loopback-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
lang: en
title: 'Loopback Component'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Loopback-component.html
---

This section details a few useful and essential Loopback components.

- [**Authorisation**](./Loopback-component-authorisation.md)
9 changes: 9 additions & 0 deletions docs/site/sidebars/lb4_sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,15 @@ children:
url: Testing-your-extension.html
output: 'web, pdf'

- title: 'Loopback Component'
url: Loopback-component.html
output: 'web, pdf'
children:

- title: 'Authorisation'
url: Loopback-component-authorisation.html
output: 'web, pdf'

- title: 'Crafting LoopBack 4'
url: Crafting-LoopBack-4.html
output: 'web, pdf'
Expand Down

0 comments on commit 07dfb7b

Please sign in to comment.