Page Contents

In page Authentication component, we introduced “authentication strategy” as the system’s core concept which contains the logic of identity verification. And a prototype JWT strategy is provided in the JWT extension for users to try and get started quickly.

In a real world application, the identity verification could be complicated and production based, and the flexibility of having multiple strategies is required. This section explains how to create and provide your own authentication strategies.

Create a Custom Strategy

Support for multiple authentication strategies is possible with a common authentication strategy interface, and an extensionPoint/extensions pattern used to register and discover authentication strategies.

The AuthenticationComponent declares a common authentication strategy interface named AuthenticationStrategy.

export interface AuthenticationStrategy {
  /**
   * The 'name' property is a unique identifier for the
   * authentication strategy (for example: 'basic', 'jwt', etc)
   */
  name: string;

  /**
   * The 'authenticate' method takes in a given request and returns a user profile
   * which is an instance of 'UserProfile'.
   * (A user profile is a minimal subset of a user object)
   * If the user credentials are valid, this method should return a 'UserProfile' instance.
   * If the user credentials are invalid, this method should throw an error
   * If the user credentials are missing, this method should throw an error, or return 'undefined'
   * and let the authentication action deal with it.
   *
   * @param request - Express request object
   */
  authenticate(request: Request): Promise<UserProfile | undefined>;
}

Developers who wish to create a custom authentication strategy must implement this interface. The custom authentication strategy must have a unique name and have an authenticate function which takes in a request and returns the user profile of an authenticated user.

Here is an example of a basic authentication strategy BasicAuthenticationStrategy with the name 'basic' in basic-strategy.ts:

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

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

  constructor(
    @inject(UserServiceBindings.USER_SERVICE)
    private userService: UserService,
  ) {}

  async authenticate(request: Request): Promise<UserProfile | undefined> {
    const credentials: Credentials = this.extractCredentials(request);
    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;
  }
}

As you can see in the example, an authentication strategy can inject custom services to help it accomplish certain tasks. See the complete examples for basic authentication strategy and jwt authentication strategy.

The AuthenticationComponent component also provides two optional service interfaces which may be of use to your application: UserService and TokenService.

Registering a Custom Authentication Strategy

The registration and discovery of authentication strategies is possible via the Extension Point and Extensions pattern.

You don’t have to worry about the discovery of authentication strategies, this is taken care of by AuthenticationStrategyProvider which resolves and returns an authentication strategy of type AuthenticationStrategy.

The AuthenticationStrategyProvider class (shown below) declares an extension point named AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME via the @extensionPoint decorator. The binding scope is set to transient because an authentication strategy may differ with each request.

With the aid of metadata of type AuthenticationMetadata (provided by AuthMetadataProvider and injected via the AuthenticationBindings.METADATA binding key), the name of the authentication strategy, specified in the @authenticate decorator for this request, is obtained.

Then, with the aid of the @extensions() getter decorator, AuthenticationStrategyProvider is responsible for finding and returning the authentication strategy which has that specific name and has been registered as an extension of the aforementioned extension point.

@extensionPoint(
  AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
  {scope: BindingScope.TRANSIENT},
) //this needs to be transient, e.g. for request level context.
export class AuthenticationStrategyProvider
  implements Provider<AuthenticationStrategy | undefined>
{
  constructor(
    @extensions()
    private authenticationStrategies: Getter<AuthenticationStrategy[]>,
    @inject(AuthenticationBindings.METADATA)
    private metadata?: AuthenticationMetadata,
  ) {}
  async value(): Promise<AuthenticationStrategy | undefined> {
    if (!this.metadata) {
      return undefined;
    }
    const name = this.metadata.strategy;
    const strategy = await this.findAuthenticationStrategy(name);
    if (!strategy) {
      // important to throw a non-protocol-specific error here
      let error = new Error(`The strategy '${name}' is not available.`);
      Object.assign(error, {
        code: AUTHENTICATION_STRATEGY_NOT_FOUND,
      });
      throw error;
    }
    return strategy;
  }

  async findAuthenticationStrategy(name: string) {
    const strategies = await this.authenticationStrategies();
    const matchingAuthStrategy = strategies.find(a => a.name === name);
    return matchingAuthStrategy;
  }
}

In order for your custom authentication strategy to be found, it needs to be registered.

Registering a custom authentication strategy BasicAuthenticationStrategy as an extension of the AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME extension point in an application application.ts is as simple as:

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

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

    //...

    registerAuthenticationStrategy(this, BasicAuthenticationStrategy);

    //...
  }
}

Using Passport-based Strategies

The earlier version of @loopback/authentication is based on an express middleware called passport, which supports 500+ passport strategies for verifying an express app’s requests. In @loopback/authentication@2.0, we defined our own interface AuthenticationStrategy that describes a strategy with different contracts than the passport strategy, but we still want to keep the ability to support those existing 500+ community passport strategies. Therefore, we rewrote the adapter class. It now converts a passport strategy to the one that LoopBack 4 authentication system expects and it was released in a new package @loopback/authentication-passport.

Creating and registering a passport strategy is explained in the README.md file

The usage of authentication decorator and the change in sequence stay the same.