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 setupManagementAuthentication and setupEndUserAuthentication.

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;
}

SubjectType

SubjectType is an enumeration that is used to identify different types of authenticated tokens. A property of this type named subjectType is available in every Mosaic user token.

enum SubjectType {
  ManagedServiceAccount = 'ManagedServiceAccount',
  ServiceAccount = 'ServiceAccount',
  UserAccount = 'UserAccount',
  ImpersonatedUserAccount = 'ImpersonatedUserAccount',
  SuperUserAccount = 'SuperUserAccount',
  EnvironmentAdminAccount = 'EnvironmentAdminAccount',
  EndUserAccount = 'EndUserAccount',
  EndUserApplication = 'EndUserApplication',
}

GenericAuthenticatedSubject

This interface abstracts common properties that are included in a JWT. This is a private interface.

interface GenericAuthenticatedSubject extends EnvironmentInfo {
  iat: number;
  exp: number;
  aud: string;
  iss: string;
  sub: string;
  name: string;
  email?: string;
  subjectType: SubjectType;
}

AuthenticatedManagementSubject

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

interface AuthenticatedManagementSubject
  extends GenericAuthenticatedSubject {
  permissions: {
    [key: string]: string[];
  };
  tags?: string[];
  [key: string]: unknown;
}

AuthenticatedEndUser

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

interface AuthenticatedEndUser extends GenericAuthenticatedSubject {
  applicationId: string;
  sessionId: string;
  profileId: string;
  extensions: unknown;
}

ManagementAuthenticationContext

This interface describes the properties related to a management user authentication. It has an AuthenticatedManagementSubject if the authentication was successful or an AxGuardErrorInfo object if the authentication failed.

interface ManagementAuthenticationContext {
  subject?: AuthenticatedManagementSubject;
  authErrorInfo?: AxGuardErrorInfo;
}

AuthenticatedManagementRequest

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

interface AuthenticatedManagementRequest extends Request {
  authContext: ManagementAuthenticationContext;
}

EndUserAuthenticationContext

This interface describes the properties related to end-user authentication. It has an AuthenticatedEndUser if the authentication was successful or an AxGuardErrorInfo object if the authentication failed.

interface EndUserAuthenticationContext {
  subject?: AuthenticatedEndUser;
  authErrorInfo?: AxGuardErrorInfo;
}

AuthenticatedEndUserRequest

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

interface AuthenticatedEndUserRequest extends Request {
  authContext: EndUserAuthenticationContext;
}

PermissionDefinition

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

This structure can store an array of Permission objects, and have some configuration properties specially when it comes to GraphQL operations.

  • anonymousGqlOperations - Operations defined under this property is ignored while checking for authorization.

  • ignoredGqlOperations - Operations defined under this property are always disabled and the EnforceStrictPermissions plugin will not produce any warnings in the logs. This option can be used when a developer is certain that a GraphQL operation must not be available in the API and therefore explicitly defines it as an ignored operations, so the default behavior of warning about it in the logging will be turned off.

interface Permission {
  /**
    *  Key of the permission.
    * @property
    */
  key: string;
  /**
    * Title of the permission.
    * @property
    */
  title: string;
  /**
    * True for only permission is used for manage services.
    * @property
    */
  usedByManagedServiceOnly?: boolean;
  /**
    * True for only permission is used for development.
    * @property
    */
  usedForDevelopment?: boolean;
  /**
    * List of GraphQL operations guarded by this Permission.
    * @property
    */
  gqlOperations?: readonly string[];
}

interface PermissionDefinition {
  gqlOptions?: {
    /**
      * Any operations defined under ANONYMOUS will be ignored while checking for Authorization.
      * @property
      */
    anonymousGqlOperations?: string[];
    /**
      * Any operations defined under IGNORE will not be logged as disabled operations
      * as they are known to be intentionally removed.
      * @property
      */
    ignoredGqlOperations?: string[];
  };

  /**
    * Array of permissions owned by the service. These permissions will be available to be granted to
    * User Roles and Service Accounts.
    * @property
    */
  permissions: readonly Permission[];
}

Permission Naming Convention

The permissions for the managed services have a naming convention as follows:

  • Key - <PLURAL_ENTITY>_<ACTION>

  • Title - <Plural Entity>: Action

It is advised to follow a similar naming convention when defining customizable service permissions to have better consistency, but is not enforced in anyway.

An example permission definition using this naming convention can be seen below:

export const permissionDefinition: PermissionDefinition = {
  gqlOptions: {
      anonymousGqlOperations: [M.someOperationWhichCanBeCalledWithoutAnyToken],
  },

  permissions: [
    {
      key: 'MOVIES_VIEW',
      title: 'Movies: View',
      gqlOperations: [...MoviesReadOperations],
    },
    {
      key: 'MOVIES_EDIT',
      title: 'Movies: Edit',
      gqlOperations: [...MoviesReadOperations, ...MoviesMutateOperations],
    }
  ]
}

Middleware

setupManagementAuthentication

function setupManagementAuthentication(
  app: Express,
  guardRoutes: string[],
  authParams: string | AuthenticationConfig,
): void;

This is the Express middleware that is used by Mosaic services to secure any http endpoints they expose that are related to management users. 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 AuthenticatedManagementRequest. AuthenticatedManagementRequest 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 AuthenticatedManagementSubject.

Usage

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

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

The middleware function takes the three following arguments:

  • Express app object (app)

  • Guard routes (['/graphql'])

  • A string or an AuthenticationConfig object (authParams)

If a string is passed as the authParams argument, it should contain the ID Service Auth Endpoint. If it is an instance of AuthenticationConfig, it should contain ID Service connection information. In case of non-managed services, it is mandatory to pass an AuthenticationConfig object with the correct tenant and environment IDs.

Since a regular Mosaic backend only exposes GraphQL endpoints, usually, the setupManagementAuthentication 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 errors with the following codes while validating the JWT:

  • AccessTokenExpired - This error is thrown if the passed JWT has expired

  • SigningKeyNotFound - This error is thrown if a valid signing key is not found to verify the token. This may happen if the signing key used to sign the token is revoked.

  • JwksError - This error is thrown if an error occurs while trying to fetch the signing keys from the JWKS endpoint. One of the reasons this might happen is because the library cannot find a signing key for the kid in the JWT, because the keys were rotated and the user is still trying to access using a JWT signed using the old key. Signing in again to the application may resolve the error.

  • IdentityServiceNotAccessible - This error is thrown if there is a network level error while trying to access the ID Service for verification. i.e. ECONNREFUSED

  • AccessTokenVerificationFailed - This error is thrown if the error does not belong to any of the above categories.

setupEndUserAuthentication

function setupEndUserAuthentication(
  app: Express,
  guardRoutes: string[],
  authParams: string | AuthenticationConfig,
): void;

This is the Express middleware that is used by Mosaic services to secure any http endpoints they expose that are related to end-user related services. It expects a bearer token embedded in the http request header signed by the user service. The function validates the bearer token 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 AuthenticatedEndUserRequest. AuthenticatedEndUserRequest 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 AuthenticatedEndUser.

Usage

The setupEndUserAuthentication middleware is called in the index.ts file of end-user facing Mosaic services that require the authentication functionality.

setupEndUserAuthentication(app, ['/graphql'], config.userServiceAuthEndpoint);

The middleware function takes the following three arguments:

  • Express app object (app)

  • Guard routes (['/graphql'])

  • A string or an AuthenticationConfig object (authParams)

If a string is passed as the authParams argument, it should contain the User Service Auth Endpoint. If it is an instance of AuthenticationConfig, it should contain User Service connection information. In case of non-managed services, it is mandatory to pass an AuthenticationConfig object with the correct tenant and environment IDs.

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

Since a regular Mosaic backend only exposes GraphQL endpoints, usually, the setupEndUserAuthentication 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 - This error is thrown if the passed JWT has expired

  • SigningKeyNotFound - This error is thrown if a valid signing key is not found to verify the token. This may happen if the signing key used to sign the token is revoked.

  • JwksError - This error is thrown if an error occurs while trying to fetch the signing keys from the JWKS endpoint. One of the reasons this might happen is because the library cannot find a signing key for the kid in the JWT, because the keys were rotated and the user is still trying to access using a JWT signed using the old key. Signing in again to the application may resolve the error.

  • UserServiceNotAccessible - This error is thrown if there is a network level error while trying to access the User Service for verification. i.e. ECONNREFUSED

  • AccessTokenVerificationFailed - This error is thrown if the error does not belong to any of the above categories.

setupManagementGQLSubscriptionAuthentication

setupManagementGQLSubscriptionAuthentication = (
  authParams: string | AuthenticationConfig,
): Middleware<Request, Response>[];

This returns an array of Express middleware that is used by Mosaic services for setting up authentication for GraphQL subscriptions that are related to management services.

The middleware returned by this function first extracts the bearer token from the request, parses the token to verify it and attaches it to the authContext property of the request as an ManagementAuthenticationContext. If the authErrorInfo property has a value then that error is thrown.

Usage

setupManagementGQLSubscriptionAuthentication should be passed as websocketMiddleware when setting up web socket server using setupHttpServerWithWebsockets().

const httpServer = setupHttpServerWithWebsockets(
    app,
    logger,
    setupManagementGQLSubscriptionAuthentication(
      config.idServiceAuthEndpointUrl,
    ),
  );

This function takes a single argument of either a string which contains the URL of the ID Service auth endpoint or an instance of AuthenticationConfig containing ID Service connection information.

Errors

This middleware may throw an AxGuardError if the token verification fails.

setupEndUserGQLSubscriptionAuthentication

setupEndUserGQLSubscriptionAuthentication = (
  authParams: string | AuthenticationConfig,
): Middleware<Request, Response>[];

This returns an array of Express middleware that is used by Mosaic services for setting up authentication for GraphQL subscriptions that are related to end-user applications.

The middleware returned by this function first extracts the bearer token from the request, parses the token to verify it and attaches it to the authContext property of the request as an EndUserAuthenticationContext. If the authErrorInfo property has a value then that error is thrown.

Usage

setupEndUserGQLSubscriptionAuthentication should be passed as websocketMiddleware when setting up web socket server using setupHttpServerWithWebsockets().

const httpServer = setupHttpServerWithWebsockets(
    app,
    logger,
    setupEndUserGQLSubscriptionAuthentication(
      config.userServiceAuthEndpointUrl,
    ),
  );

This function takes a single argument of either a string which contains the URL of the User Service auth endpoint or an instance of AuthenticationConfig containing User Service connection information.

Errors

This middleware may throw an AxGuardError if the token verification fails.

Functions

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

getAuthenticatedManagementSubject

This function validates the specified token in the context of a given environment and derives an AuthenticatedManagementSubject object (which contains a list of the permissions provided in the token).

getAuthenticatedManagementSubject = async (
  token: string,
  authConfig: AuthenticationConfig,
): Promise<AuthenticatedManagementSubject>

getAuthenticatedEndUser

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

getAuthenticatedEndUser = async (
  token: string,
  authParams: string | AuthenticationConfig,
): Promise<AuthenticatedEndUser>

getManagementAuthenticationContext

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

getManagementAuthenticationContext = async (
  req: Request,
  authConfig?: AuthenticationConfig,
): Promise<ManagementAuthenticationContext>

getEndUserAuthenticationContext

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

getEndUserAuthenticationContext = async (
  req: Request,
  authParams?: string | AuthenticationConfig,
): Promise<EndUserAuthenticationContext>

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. The authorization logic applies to queries, mutations and subscriptions.

In addition to performing authorization logic, this plugin generates a permission-definition.json file when the configuration is set to development. This file contains the Permission Definition for the service. If this file needs to be generated, the AxGuard plugin must be provided with two additional arguments:

  • config - The configuration object of the service.

  • permissionDefinitionJsonPath - The path to the permission-definition.json file.

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(config, './src/generated/security/permission-definition.json'),
    )
    .build();

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

The AuthenticatedManagementSubject or AuthenticatedEndUser 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 getManagementAuthenticationContext(
        req,
        ManagementAuthenticationContext,
      );
      const extendedRequest = req as Request & { token: string };
      return {
        subject,
        authErrorInfo,
      };
    })
    .build();

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

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

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

AxGuardPlugin is a wrapper plug-in. For any GraphQL request, after a request is authenticated through setupManagementAuthentication or setupEndUserAuthentication 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 anonymousGqlOperations in the permissionDefinition. Then, it extracts the permissions attached to the AuthenticatedManagementSubject object and validates against the permissionDefinition object to check if the required permissions are present.

Errors

If the required permissions are not present in the AuthenticatedManagementSubject or AuthenticatedEndUser 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 PermissionDefinition file. This excludes any operations defined under anonymousGqlOperations.

Usage

This plug-in requires the permissionDefinition 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

ManagedServiceGuardedMessageHandler<TContent>

The ManagedServiceGuardedMessageHandler is an abstract class that can be used to build message handlers. When extended from ManagedServiceGuardedMessageHandler, it changes the MessageInfo parameter to an AuthenticatedManagementSubjectMessageInfo 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 ManagedServiceGuardedMessageHandler. 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 ManagedServiceGuardedMessageHandler and that this class is used for handler creation.

// Service specific class
abstract class MediaManagedServiceGuardedMessageHandler<
  TContent
> extends ManagedServiceGuardedMessageHandler<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 MediaManagedServiceGuardedMessageHandler
class PublishEntityCommandHandler extends MediaManagedServiceGuardedMessageHandler<
  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> {
    //....
  }
}