Page Contents

Managing Custom Authentication Strategy Options

This is an optional step.

If your custom authentication strategy doesn’t require special options, you can skip this section.

As previously mentioned in the Authentication Decorator section, a custom authentication strategy should avoid repeatedly specifying its default options in the @authenticate decorator. Instead, it should define its default options in one place, and only specify overriding options in the @authenticate decorator when necessary.

Here are the steps for accomplishing this.

Default authentication metadata

In some cases, it’s desirable to have a default authentication enforcement for methods that are not explicitly decorated with @authenticate. To do so, we can simply configure the authentication component with defaultMetadata as follows:

app
  .configure(AuthenticationBindings.COMPONENT)
  .to({defaultMetadata: [{strategy: 'xyz'}]});

If multiple strategies are used for a given method, we can configure the failOnError option so that a strategy can abort the authentication process by throwing an error. Otherwise, other strategies will be invoked and it only fails when none of the strategies succeeds by returning a UserProfile or RedirectRoute.

app.configure(AuthenticationBindings.COMPONENT).to({failOnError: true});

Define the Options Interface and Binding Key

Define an options interface and a binding key for the default options of that specific authentication strategy.

export interface AuthenticationStrategyOptions {
  [property: string]: any;
}

export namespace BasicAuthenticationStrategyBindings {
  export const DEFAULT_OPTIONS =
    BindingKey.create<AuthenticationStrategyOptions>(
      'authentication.strategies.basic.defaultoptions',
    );
}

Bind the Default Options

Bind the default options of the custom authentication strategy to the application application.ts via the BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS binding key.

In this hypothetical example, our custom authentication strategy has a default option of gatherStatistics with a value of true. (In a real custom authentication strategy, the number of options could be more numerous)

export class MyApplication extends BootMixin(
  ServiceMixin(RepositoryMixin(RestApplication)),
) {
  constructor(options?: ApplicationConfig) {
    super(options);

    //...
    this.bind(BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS).to({
      gatherStatistics: true,
    });
    //...
  }
}

Override Default Options In Authentication Decorator

Specify overriding options in the @authenticate decorator only when necessary.

In this example, we only specify an overriding option {gatherStatistics: false} for the /scareme endpoint. We use the default option value for the /whoami endpoint.

import {inject} from '@loopback/context';
import {AuthenticationBindings, authenticate} from '@loopback/authentication';
import {UserProfile, securityId} from '@loopback/security';
import {get} from '@loopback/rest';

export class WhoAmIController {
  constructor(
    @inject(SecurityBindings.USER)
    private userProfile: UserProfile,
  ) {}

  @authenticate('basic')
  @get('/whoami')
  whoAmI(): string {
    return this.userProfile[securityId];
  }

  @authenticate('basic', {gatherStatistics: false})
  @get('/scareme')
  scareMe(): string {
    return 'boo!';
  }
}

Update Custom Authentication Strategy to Handle Options

The custom authentication strategy must be updated to handle the loading of default options, and overriding them if they have been specified in the @authenticate decorator.

Here is the updated BasicAuthenticationStrategy:

import {
  AuthenticationStrategy,
  TokenService,
  AuthenticationMetadata,
  AuthenticationBindings,
} from '@loopback/authentication';
import {UserProfile} from '@loopback/security';
import {Getter} from '@loopback/core';

export interface Credentials {
  username: string;
  password: string;
}

export class BasicAuthenticationStrategy implements AuthenticationStrategy {
  name: string = 'basic';

  // ------ ADD SNIPPET ---------
  @inject(BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS)
  options: AuthenticationStrategyOptions;
  // ----- END OF SNIPPET -------

  constructor(
    @inject(UserServiceBindings.USER_SERVICE)
    private userService: UserService,
    @inject.getter(AuthenticationBindings.METADATA)
    readonly getMetaData: Getter<AuthenticationMetadata>,
  ) {}

  async authenticate(request: Request): Promise<UserProfile | undefined> {
    const credentials: Credentials = this.extractCredentials(request);
    // ------ ADD SNIPPET ---------
    await this.processOptions();

    if (this.options.gatherStatistics === true) {
      console.log(`\nGathering statistics...\n`);
    } else {
      console.log(`\nNot gathering statistics...\n`);
    }
    // ----- END OF SNIPPET -------

    const user = await this.userService.verifyCredentials(credentials);
    const userProfile = this.userService.convertToUserProfile(user);

    return userProfile;
  }

  extractCredentials(request: Request): Credentials {
    let creds: Credentials;

    /**
     * Code to extract the 'basic' user credentials from the Authorization header
     */

    return creds;
  }

  async processOptions() {
    /**
        Obtain the options object specified in the @authenticate decorator
        of a controller method associated with the current request.
        The AuthenticationMetadata interface contains : strategy:string, options?:object
        We want the options property.
    */
    const controllerMethodAuthenticationMetadata = await this.getMetaData();

    if (!this.options) this.options = {}; //if no default options were bound, assign empty options object

    //override default options with request-level options
    this.options = Object.assign(
      {},
      this.options,
      controllerMethodAuthenticationMetadata.options,
    );
  }
}

Inject default options into a property options using the BasicAuthenticationStrategyBindings.DEFAULT_OPTIONS binding key.

Inject a getter named getMetaData that returns AuthenticationMetadata using the AuthenticationBindings.METADATA binding key. This metadata contains the parameters passed into the @authenticate decorator.

Create a function named processOptions() that obtains the default options, and overrides them with any request-level overriding options specified in the @authenticate decorator.

Then, in the authenticate() function of the custom authentication strategy, call the processOptions() function, and have the custom authentication strategy react to the updated options.

Summary

We’ve gone through the main steps for adding authentication to your LoopBack 4 application.

Your application.ts should look similar to this:

import {
  AuthenticationComponent,
  registerAuthenticationStrategy,
} from '@loopback/authentication';

export class MyApplication extends BootMixin(
  ServiceMixin(RepositoryMixin(RestApplication)),
) {
  constructor(options?: ApplicationConfig) {
    super(options);

    /* set up miscellaneous bindings */

    //...

    // ------ ADD SNIPPET ---------
    // load the authentication component
    this.component(AuthenticationComponent);

    // register your custom authentication strategy
    registerAuthenticationStrategy(this, BasicAuthenticationStrategy);

    // use your custom authenticating sequence
    this.sequence(MyAuthenticatingSequence);
    // ------------- END OF SNIPPET -------------

    this.static('/', path.join(__dirname, '../public'));

    this.projectRoot = __dirname;

    this.bootOptions = {
      controllers: {
        dirs: ['controllers'],
        extensions: ['.controller.js'],
        nested: true,
      },
    };
  }

You can find a completed example and tutorial of a LoopBack 4 application with JWT authentication here.