0

In loopback4, I have created custom authentication and authorization handlers, and wired them into the application. But the authorization handler is called only if the authentication function returns a UserProfile object, and skips authorization for an undefined user.

I want my Authorization handler to be called every time, no matter what the result of authentication is. I want to allow a non-authenticated call (don't know the user) to still flow through the authorization handler to let it judge whether to allow the call based on other factors besides the identity of the end user.

How do I make the Authorization handler be called every time?

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(AuthenticationBindings.AUTH_ACTION)
    protected authenticateRequest: AuthenticateFn,
  ) {}

// see: https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#adding-an-authentication-action-to-a-custom-sequence
  async handle(context: RequestContext) {
    try {
      const {request, response} = context;
      const route = this.findRoute(request);

      //call authentication action
      console.log(`request path = ${request.path}`);
      await this.authenticateRequest(request); // HOW DO I CONTROL AUTHORIZATION CALL THAT FOLLOWS?

      // Authentication step done, proceed to invoke controller
      const args = await this.parseParams(request, route);
      const result = await this.invoke(route, args);
      this.send(response, result);
    } catch (error) {
      if (
        error.code === AUTHENTICATION_STRATEGY_NOT_FOUND ||
        error.code === USER_PROFILE_NOT_FOUND
      ) {
        Object.assign(error, {statusCode: 401 /* Unauthorized */});
      }

      this.reject(context, error);
    }
  }
}

The full example of code is lengthy, so I have posted it in a gist here.

broc.seib
  • 21,643
  • 8
  • 63
  • 62

1 Answers1

0

I found one way to invoke an authorization handler for every request. This still doesn't feel quite right, so there's probably a better solution.

In the application.ts you can setup default authorization metadata and supply a simpler voter that always votes DENY. After that, all controller calls will invoke authorization handlers, whether there is a @authorize() decorator present or not. Here's the setup:

    // setup authorization
    const noWayJose = (): Promise<AuthorizationDecision> => {
      return new Promise(resolve => {
        resolve(AuthorizationDecision.DENY);
      });
    };
    this.component(AuthorizationComponent);
    this.configure(AuthorizationBindings.COMPONENT).to({
      defaultDecision: AuthorizationDecision.DENY,
      precedence: AuthorizationDecision.ALLOW,
      defaultMetadata: {
        voters: [noWayJose],
      },
    });
    this.bind('authorizationProviders.my-authorization-provider')
      .toProvider(MyAuthorizationProvider)
      .tag(AuthorizationTags.AUTHORIZER);

Now the /nope endpoint in the controller will have Authorization handlers evaluated even without the decorator.

export class YoController {
  constructor() {}

  @authorize({scopes: ['IS_COOL', 'IS_OKAY']})
  @get('/yo')
  yo(@inject(SecurityBindings.USER) user: UserProfile): string {
    return `yo, ${user.name}!`;
  }

  @authorize({allowedRoles: [EVERYONE]})
  @get('/sup')
  sup(): string {
    return `sup, dude.`;
  }

  @get('/nope')
  nope(): string {
    return `sorry dude.`;
  }

  @authorize({allowedRoles: [EVERYONE]})
  @get('/yay')
  yay(
    @inject(SecurityBindings.USER, {optional: true}) user: UserProfile,
  ): string {
    if (user) {
      return `yay ${user.name}!`;
    }
    return `yay!`;
  }
}

The other thing you have to do is not throw an error when authentication fails to find a user. That's because authorization does not get exercised until the invoke() function calls all the interceptors. So you have to swallow that error and let authorization have a say:

  // from sequence.ts
  async handle(context: RequestContext) {
    try {
      const {request, response} = context;
      const route = this.findRoute(request);

      //call authentication action
      console.log(`request path = ${request.path}`);
      try {
        await this.authenticateRequest(request);
      } catch (authenticationError) {
        if (authenticationError.code === USER_PROFILE_NOT_FOUND) {
          console.log(
            "didn't find  user. let's wait and see what authorization says.",
          );
        } else {
          throw authenticationError;
        }
      }

      // Authentication step done, proceed to invoke controller
      const args = await this.parseParams(request, route);

      // Authorization happens within invoke()
      const result = await this.invoke(route, args);
      this.send(response, result);
    } catch (error) {
      if (
        error.code === AUTHENTICATION_STRATEGY_NOT_FOUND ||
        error.code === USER_PROFILE_NOT_FOUND
      ) {
        Object.assign(error, {statusCode: 401 /* Unauthorized */});
      }

      this.reject(context, error);
    }
  }

This is all suited to my use case. I wanted global defaults to have every endpoint be locked down with zero @authenticate and @authorize() decorators present. I plan to only add @authorize() to those places where I want to open things up. This is because I'm about to auto-generate a ton of controllers and will only want to expose a portion of the endpoints by hand.

broc.seib
  • 21,643
  • 8
  • 63
  • 62
  • To be clear, if the `noWayJose` voter is removed, then the undecorated `/nope` endpoint will never run the authorization code supplied by `MyAuthorizationProvider`. But when the `noWayJose` voter is supplied, then *both* authorization functions are executed. That might be a bug in behavior. If so, the problem is at line 63 here: https://github.com/strongloop/loopback-next/blob/56543fe1e844366faa6274e6d5cf10f15ec27d91/packages/authorization/src/authorize-interceptor.ts#L62-L67 – broc.seib Jan 30 '20 at 15:41