Page Contents

Users are expected to program policies that enforce access control in two of the following options:

  • Authorizer functions
    • The authorizer functions are applied globally, i.e, they are enforced on all endpoints in the application
  • Voter functions
    • voters are specific for the endpoint that is decorated with it
    • multiple voters can be configured for an endpoint

Usually the authorize functions are bound through a provider as below

  • The AuthorizationContext parameter of the authorize function contains the current principal (in the example given above,that would be the current user invoking cancelOrder) and details of the invoked endpoint.
    • The AuthorizationMetadata parameter of the authorize function contains all the details provided in the invoked method’s decorator.
class MyAuthorizationProvider implements Provider<Authorizer> {
  /**
   * @returns an authorizer function
   *
   */
  value(): Authorizer {
    return this.authorize.bind(this);
  }

  async authorize(
    context: AuthorizationContext,
    metadata: AuthorizationMetadata,
  ) {
    events.push(context.resource);
    if (
      context.resource === 'OrderController.prototype.cancelOrder' &&
      context.principals[0].name === 'user-01'
    ) {
      return AuthorizationDecision.DENY;
    }
    return AuthorizationDecision.ALLOW;
  }
}

the authorize function is then tagged to an application as AuthorizationTags.AUTHORIZER as below.

import {AuthorizationTags} from '@loopback/authorization';
let app = new Application();
app
  .bind('authorizationProviders.my-provider')
  .toProvider(MyAuthorizationProvider)
  .tag(AuthorizationTags.AUTHORIZER);
  • This creates a list of authorize() functions.
    • The authorize(AuthorizationContext, AuthorizationMetadata) function in the provider class is expected to be called by the Authorization Interceptor which is called for every API endpoint decorated with @authorize().
    • The authorize interceptor gets the list of functions tagged with AuthorizationTags.AUTHORIZER (and also the voters listed in the @authorize decorator per endpoint) and calls the functions one after another.
    • The authorize() function is expected to return an object of type AuthorizationDecision. If the type returned is AuthorizationDecision.ALLOW the current Principal has passed the executed authorize() function’s criteria.

Voter functions are directly provided in the decorator of the remote method

  async function compareId(
    authorizationCtx: AuthorizationContext,
    metadata: MyAuthorizationMetadata,
  ) {
    let currentUser: UserProfile;
    if (authorizationCtx.principals.length > 0) {
      const user = _.pick(authorizationCtx.principals[0], [
        'id',
        'name',
        'email',
      ]);
      return AuthorizationDecision.ALLOW;
    } else {
      return AuthorizationDecision.DENY;
    }
  }

  @authenticate('jwt')
  @authorize({resource: 'order', scopes: ['patch'], voters: [compareId]})
  async patchOrders(
    @param.path.string('userId') userId: string,
    @requestBody() order: Partial<Order>,
    @param.query.string('where') where?: Where<Order>,
  ): Promise<Count> {
    return this.userRepo.orders(userId).patch(order, where);
  }

In the above example compareId() is an authorizing function which is provided as a voter in the decorator for the patchOrders() method.