The package @axinom/mosaic-id-guard provides the required middleware and utility related to securing GraphQL endpoints and messages, security permissions, authentication and authorization

@axinom/mosaic-id-guard

The mosaic-id-guard package provides the required middleware and utility related to securing GraphQL endpoints and messages, security permissions, authentication and authorization. These utilities can be used for authentication purposes anywhere within the Mosaic framework.

The exported interfaces, functions, and types are described below.

Interfaces

This section describes the exported interfaces through mosaic-id-guard. These interfaces are used to enforce the data structures required in the library.

AuthenticationConfig

This interface describes the information required to access the Identity Service. This is used in utility methods, such as setupAuthentication.

interface AuthenticationConfig {
  authEndpoint: string;
  tenantId: string;
  environmentId: string;
}

EnvironmentInfo

This interface describes a specific environment in Mosaic using tenantId and environmentId.

interface EnvironmentInfo {
  tenantId: string;
  environmentId: string;
}

AuthenticatedSubject

This interface describes information related to an authenticated user. An instance of this object is constructed by parsing the values extracted from the JWT.

interface AuthenticatedSubject extends EnvironmentInfo {
  iat: number;
  exp: number;
  aud: string;
  iss: string;
  sub: string;
  name: string;
  email?: string;
  permissions: {
    [key: string]: string[];
  };
  tags?: string[];
  subjectType: SubjectType;
  [key: string]: unknown;
}

AuthenticationContext

This interface describes the properties related to an authentication. It has an AuthenticatedSubject if the authentication was successful or an AxGuardErrorInfo object if the authentication failed.

interface AuthenticationContext {
  subject?: AuthenticatedSubject;
  authErrorInfo?: AxGuardErrorInfo;
}

AuthenticatedRequest

This is an extended type of the Express req object with the additional property authContext which is of type AuthenticationContext.

interface AuthenticatedRequest extends Request {
  authContext: AuthenticationContext;
}

IOperationsToPermissionMappings

This interface is used to construct an object containing permission names along with the GraphQL operations that belong to a permission.

There are three well-known permission types that have a specific behavior:

  • ANONYMOUS - Any operations defined under this permission type is ignored while checking for authorization.

  • IGNORE - Operations defined under IGNORE are disabled and not exposed via the GraphQL endpoint.

  • ADDITIONAL - Any operations that are omitted at the root level ( i.e. by smart tags), but are used as either a ForwardRelation or a BackwardRelation field in a nested level must be defined here, in addition to the respective permissions they belong to. Mosaic uses this to void these operations from being logged as surplus.

interface IOperationsToPermissionMappings {

  ANONYMOUS: string[];

  IGNORE: string[];

  ADDITIONAL: string[];
  /**
   * Map of service-specific permissions.
   * It's also possible to have a permission name with an empty array of operations.
   * This is especially useful in authorizing REST APIs where a route could be authorized if an access-token
   * contains a specific permission, but there is no real need for mapping any GraphQL operations to that permission.
   * @property
   */
  [key: string]: string[];
}

Middleware

setupAuthentication

function setupAuthentication(
  app: Express,
  guardRoutes: string[],
  authenticationConfig: AuthenticationConfig,
): void

This is the Express middleware that is used by Mosaic services to secure any http endpoints they expose. It validates the Bearer token embedded in the http request header and authorizes the request to use the routes passed as an argument. If a Bearer token is not present for an endpoint guarded by this middleware it throws an AccessTokenRequired error.

In the validation process, it extracts the JWT, verifies its authenticity and sets the authContext of the AuthenticatedRequest. AuthenticatedRequest is an extended type of the Express req object with an additional authContext property. The authContext is an object that carries information related to the authenticated subject of type AuthenticatedSubject.

Usage

The setupAuthentication middleware is called in the index.ts file of Mosaic services that require the authentication functionality.

setupAuthentication(app, ['/graphql'], authenticationConfig);

The middleware function takes the three following arguments:

  • Express app object (app)

  • Guard routes (['/graphql'])

  • AuthenticationConfig object (authenticationConfig)

In practice, this is called before the call to the setupPostGraphile middleware.

Since a regular Mosaic backend only exposes GraphQL endpoints, usually, the setupAuthentication middleware is only registered for the /graphql endpoint. However, that does not restrict an application developer from using the middleware to secure any other endpoints that the application might expose.

Errors

This middleware may throw the following errors while validating the JWT:

  • AccessTokenExpired

  • SigningKeyNotFound

  • JwksError

  • AccessTokenVerificationFailed

Functions

The following section describes the functions that are exported through this library as well as their usages.

getAuthenticatedSubject

This function can be used to verify and derive an AuthenticatedSubject object from a JWT.

getAuthenticatedSubject = async (
  token: string,
  authConfig: AuthenticationConfig,
): Promise<AuthenticatedSubject>

getAuthenticationContext

This function accepts an http request and an AuthenticationConfig object and returns the AuthenticationContext for that request.

getAuthenticationContext = async (
  req: Request,
  authConfig?: AuthenticationConfig,
): Promise<AuthenticationContext>

PostGraphile Plug-ins

The PostGraphile plug-ins are used to extend the GraphQL schema that is generated by PostGraphile.

AxGuardPlugin

The AxGuardPlugin is a Postgraphile wrapper plug-in which should be added to the appendPlugins array in the PostGraphile options object when setting up PostGraphile. This plug-in takes care of the authorization functionality and makes sure that the JWT has the required permissions to access the given GraphQL resource.

Setting up PostGraphile

The PostGraphile options can be set up in two ways:

  • Construct the PostGraphile options as mentioned here.

  • Use the PostgraphileOptionsBuilder exposed via @axinom/mosaic-service-common to construct the PostGraphile options object.

Usage

Note
Extra code has been removed from the examples below for readability. All examples use the PostgraphileOptionsBuilder to construct the PostGraphile options.

The plug-in is added to the PostGraphile options as shown below.

PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
    .addPlugins(
      AxGuardPlugin,
    )
    .build();

There are a few pre-requisites for the AxGuardPlugin to work, which need to be set in the PostGraphile options.

The AuthenticatedSubject must be set in the ExtendedGraphQLContext. It can be done as shown below.

PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
    .setAdditionalGraphQLContextFromRequest(async (req) => {
      const { subject, authErrorInfo } = await getAuthenticationContext(
        req,
        authenticationContext,
      );
      const extendedRequest = req as Request & { token: string };
      return {
        subject,
        authErrorInfo,
      };
    })
    .build();

The serviceId and permissionMappings properties must be set in the PostGraphile options. The permissionMappings object is of the type IOperationsToPermissionMappings, which contains the mapping of permissions to GraphQL operations.

The following code snippet describes how serviceId and permissionMappings are set.

PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
    .addGraphileBuildOptions({
          serviceId: config.serviceId,
          permissionMappings: OperationsToPermissionMappings,
        })
    .build();

AxGuardPlugin is a wrapper plug-in. For any GraphQL request, after a request is authenticated through the setupAuthentication middleware, this plug-in comes into action and performs the required authorization tasks. The plug-in is executed before the rest of the request is processed.

When performing the authorization, it excludes any operations that are defined under ANONYMOUS in the permissionMappings. Then, it extracts the permissions attached to the AuthenticatedSubject object and validates against the permissionMappings object to check if the required permissions are present.

Errors

If the required permissions are not present in the AuthenticateSubject to perform the requested GraphQL operation, the plug-in throws the UserNotAuthorized error.

EnforceStrictPermissionsPlugin

The EnforceStrictPermissionsPlugin omits all GraphQL operations from being exposed that are not assigned to a permission in the respective IOperationsToPermissionMappings file. This excludes any operations defined under ANONYMOUS.

Usage

This plug-in requires the permissionMappings property in the PostGraphile options to be set (see above).

Then, the plug-in must added to the appendPlugins array in the PostGraphile options, so it could be enabled.

PostgraphileOptionsBuilder(config.isDev, config.graphqlGuiEnabled)
    .addPlugins(
      EnforceStrictPermissionsPlugin,
    )
    .build();

When the service is started, this plug-in logs any disabled operations and mapped operations that do not exist.

Classes

GuardedMessageHandler<TContent>

The GuardedMessageHandler is an abstract class that can be used to build message handlers. When extended from GuardedMessageHandler, it changes the MessageInfo parameter to an AuthenticatedMessageInfo object, which contains a verified JWT subject.

In addition, it validates if the required set of permissions defined for the message handler are present.

Usage

Any message handler that requires authorization must be extended from GuardedMessageHandler. In the constructor, a call to super() must be made, defining permissions that are required for the message handler to function.

As a practice/pattern, it is recommended that a service-specific class is defined extending from the GuardedMessageHandler and that this class is used for handler creation.

// Service specific class
abstract class MediaGuardedMessageHandler<
  TContent
> extends GuardedMessageHandler<TContent> {
  constructor(
    messagingKey: string,
    permissions: Permission[],
    protected readonly config: Config,
    overrides?: SubscriptionConfig,
    middleware: OnMessageMiddleware[] = [],
  ) {
    super(
      messagingKey,
      permissions,
      config.serviceId,
      {
        tenantId: config.tenantId,
        environmentId: config.environmentId,
        authEndpoint: config.idServiceAuthEndpointUrl,
      },

      overrides,
      middleware,
    );
  }
}

// Message handler extending from MediaGuardedMessageHandler
class PublishEntityCommandHandler extends MediaGuardedMessageHandler<
  PublishEntityCommand
> {
  private readonly logger;

  constructor(
    protected readonly broker: Broker,
    private readonly dbPool: Pool,

    config: Config,
  ) {
    super(
      // Defining permissions required to execute the message handler
      MediaMessagingSettings.PublishEntity.messageType,
      [
        'ADMIN',
        'COLLECTIONS_EDIT',
        'MOVIES_EDIT',
        'SETTINGS_EDIT',
        'TVSHOWS_EDIT',
      ],
      config,
    );

    this.logger = new Logger(config, 'PublishEntityCommandHandler');
  }

  async onMessage(
    payload: PublishEntityCommand,
    messageInfo: MessageInfo<PublishEntityCommand>,
  ): Promise<void> {
    //....
  }
}