DEV Community

Cover image for Create NestJS libraries to interact with Ory API
Edouard Maleix
Edouard Maleix

Posted on • Edited on • Originally published at push-based.io

Create NestJS libraries to interact with Ory API

Ory offers excellent documentation but needs more support tools and in-depth examples of using its libraries in TypeScript and NestJS projects. I decided to contribute to it by creating a set of libraries to interact with APIs, which will (hopefully) make integration into your NestJS project easier. This post presents the ideal use case to divulge my routines for creating libraries in NestJS/Nx!

Feel free to skip the journey and go straight to the code!

Note
This article is part of a series on integrating Ory in production with NestJS. If you are interested in the other articles, you can find them here.

The plan

I will follow the recipe from a previous blog post to create the libraries.

This workspace is composed of three public libraries (at least for the moment) and one private library:

  • kratos-client-wrapper is a set of NestJS modules that wraps @ory/client and, more particularly, the Frontend and Identity APIs, which are part of Ory Kratos
  • keto-client-wrapper is also a set of NestJS modules that wraps @ory/client's Permission and Relationship APIs, which are part of Ory Keto
  • base-client-wrapper is an internal NestJS module that provides a base class that the Ory client wrappers can extend (similar to the BaseApi class in @ory/client)
  • keto-relations-parser is a node library that allows manipulating relations tuples using Ory Permission Language notation. This library is an improved version of this existing lib.

Edit
Recently, I created three extra packages, keto-cli and kratos-cli, to interact with the Ory APIs from the command line and hydra-client-wrapper. They are not covered in this article.

β€”β€”

Now, let’s focus on the Ory API integration. Ory already provides an auto-generated client based on their Open API specifications, which uses axios under the hood to send HTTP requests. How can we make this an even better experience for NestJS users? I would say:

  • Easily importable and configurable module and services
  • Mockable dependencies for testing
  • Module and services with clear boundaries and simple interfaces
  • Error handling that is consistent across all modules and services
  • A way to automatically retry requests in case of rate-limiting
  • NestJS Guards to check user sessions and permissions on endpoints

Module organization

Ory services usually have a public API and an admin API.

Ory Kratos' public API allows users to register accounts, log in, and check user sessions. On the other hand, the admin API enables the management of users (called identities in Ory), authentication methods, and sessions. Ory Keto's public API allows end users to check permissions while the admin API manages relationships between entities (called namespaces in Ory).

A logical path to split our modules is to follow this organization; it would result in the following :

Ory service Library Public API Admin API
Ory Kratos kratos-client-wrapper OryFrontendModule OryIdentitiesModule
Ory Keto keto-client-wrapper OryPermissionsModule OryRelationshipsModule

As said previously, I wanted to easily mock dependencies for testing; moreover, to improve error response handling for calls to Ory APIs, providing a custom axios instance can be helpful.

Luckily, the BaseApi class from @ory/client, which all APIs extend, allows you to pass a custom axios instance in the constructor.
One solution I thought of is injecting the HttpService from @nestjs/axios into the NestJS services and passing the HttpService.axiosRef to the BaseApi constructor. This Axios instance could have custom interceptors to wrap errors in a standard class and allow adding a retry strategy in case of rate-limiting.
Since all services implement that logic, I might as well build a generic class that all others extend; sometimes, strong coupling is for a good cause.

At the end of this article, the dependency graph will look like this:
nx graph

Note
This graph was generated using Nx Graph CLI.

base-client-wrapper implementation

The starting point is the generic module, OryBaseModule, with its OryBaseService provider, and the first step will be to install dependencies:

npm i @ory/client @nestjs/axios
Enter fullscreen mode Exit fullscreen mode

Create interfaces

To configure and implement the retry logic, I needed to extend the AxiosRequestConfig interface, which axios does not support.
I created an OryAxiosRequestConfig interface that extends it and an OryBaseModuleOptions class that implements the IOryBaseModuleOptions interface. The latter is used to configure the OryBaseModule and is provided by the library consumer, in this case, the kratos-client-wrapper and keto-client-wrapper modules.

// from https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/base-client-wrapper/src/lib/ory-base.interfaces.ts
import type { AxiosError, AxiosRequestConfig } from 'axios';

export interface AxiosExtraRequestConfig {
  retries?: number;
  retryCondition?: (error: AxiosError) => boolean;
  retryDelay?: (error: AxiosError, retryCount: number) => number;
}

export type OryAxiosRequestConfig = AxiosRequestConfig &
  AxiosExtraRequestConfig;

export interface IOryBaseModuleOptions {
  axiosConfig?: OryAxiosRequestConfig;
}
//...
Enter fullscreen mode Exit fullscreen mode

To make the axios types aware of the new options, I extended the AxiosRequestConfig interface using TypeScript's declaration merging feature.

// from https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/base-client-wrapper/src/lib/ory-base.module.ts
//...
declare module 'axios' {
  interface AxiosRequestConfig extends AxiosExtraRequestConfig {}
}
//...
Enter fullscreen mode Exit fullscreen mode

OryError

To improve error handling on the consumer side. I created an OryError class that extends the JS Error to wrap AxiosError and make them more readable.
It allows consumers to check if an error is an OryError and to access the AxiosError instance.

import type { AxiosError } from 'axios';

export function isAxiosError(error: unknown): error is AxiosError {
  return (
    (typeof error === 'object' &&
      !!error &&
      (error as AxiosError).isAxiosError) ??
    false
  );
}

export class OryError
  extends Error
  implements Pick<AxiosError, 'config' | 'toJSON' | 'isAxiosError'>
{
  constructor(
    readonly error: unknown,
    readonly defaultStatus = 500,
    readonly defaultReason = 'Error connecting to the Ory API'
  ) {
    super(OryError.parseError(error));
    Object.setPrototypeOf(this, OryError.prototype);
  }

  static parseError(error: unknown): string {
    if (isAxiosError(error)) {
      return error.cause?.message ?? error.message;
    }
    if (isOryError(error)) {
      return error.errorMessage;
    }
    if (error instanceof Error) {
      return error.message;
    }
    return 'Unknown error';
  }

  get isAxiosError(): boolean {
    return isAxiosError(this.error);
  }

  toJSON(): {
    message: string;
    statusCode: number;
    details: Record<string, unknown>;
  } {
    return {
      message: this.errorMessage,
      statusCode: this.statusCode,
      details: this.getDetails(),
    };
  }

  get config(): AxiosError['config'] {
    if (isAxiosError(this.error)) {
      return this.error.config;
    }
    if (isOryError(this.error)) {
      return this.error.config;
    }
    return undefined;
  }

  get errorMessage(): string {
    return OryError.parseError(this.error) ?? this.defaultReason;
  }

  get statusCode(): number {
    if (isAxiosError(this.error)) {
      return this.error.response?.status ?? this.defaultStatus;
    }
    if (isOryError(this.error)) {
      return this.error.statusCode;
    }
    return this.defaultStatus;
  }

  getDetails(): Record<string, unknown> {
    if (isAxiosError(this.error)) {
      return (this.error.response?.data ?? {}) as Record<string, unknown>;
    }
    if (isOryError(this.error)) {
      return this.error.getDetails();
    }
    return {};
  }

  serializeErrors(): { message: string; field?: string }[] {
    return [{ message: this.errorMessage }];
  }
}

export function isOryError(error: unknown): error is OryError {
  return error instanceof OryError;
}
Enter fullscreen mode Exit fullscreen mode

OryBaseService

OryBaseService is the base class for all services interacting with Ory APIs.
It requires injecting the HttpService, configuring the axios interceptor during module initialization, and providing the axios reference via a getter.

@Injectable()
export class OryBaseService implements OnModuleInit {
  readonly logger = new Logger(OryBaseService.name);

  constructor(@Inject(HttpService) private readonly httpService: HttpService) {}

  get axios() {
    return this.httpService.axiosRef;
  }

  onModuleInit(): void {
    this.axios.interceptors.response.use(
      (response) => response,
      async (error) => {
        if ((isAxiosError(error) || isOryError(error)) && error.config) {
          const { config } = error;
          const shouldRetry =
            typeof config.retryCondition === 'function'
              ? config.retryCondition(error)
              : false;
          if (config?.retries && shouldRetry) {
            const retryDelay =
              typeof config.retryDelay === 'function'
                ? config?.retryDelay(error, config.retries)
                : 250;
            config.retries -= 1;
            this.logger.debug(
              `Retrying request to ${config.url} in ${retryDelay}ms`
            );
            await new Promise((resolve) => setTimeout(resolve, retryDelay));
            return this.httpService.axiosRef(config);
          }
          const oryError = new OryError(error);
          return Promise.reject(oryError);
        }
        const oryError = new OryError(error);
        return Promise.reject(oryError);
      }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

OryBaseModule

The OryBaseModule is a basic DynamicModule that follows the convention of using forRoot and forRootAsync static methods to configure the module.
It ensures that the HttpService provider is available for the OryBaseService in two ways:

  • With the forRoot method, the options are immediately available, so I imported the HttpModule and provided the OryBaseService with the HttpService instance
  • With the forRootAsync method, the options are provided asynchronously, so instead of importing the HttpModule, I created a custom HttpService provider that receives an Axios instance to its constructor, configured with the OryBaseModuleOptions.

Tip
Have a look at the NestJS documentation to know more about dynamic modules.

kratos-client-wrapper implementation

For this package, I created two modules (see Module organization), both of which consume the OryBaseModule.

Create OryIdentities and OryFrontend interfaces

Interfaces for these modules (OryIdentitiesModuleOptions and OryFrontendModuleOptions) extend OryBaseInterface and enable configuring the base path and the access token (for the Admin API via Ory Network) to access the Ory APIs.

Create OryIdentitiesModule and OryFrontendModule

There is nothing new here. The setup is very similar to the OryBaseModule setup. The modules import the OryBaseModule and provide the OryIdentitiesService and OryFrontendService.

Create OryIdentitiesService and OryFrontendService

Both services will depend on OryBaseService and their corresponding module options; they will extend IdentityApi and FrontendApi from @ory/client.

Create an authentication guard

To make it easier to protect endpoints with Ory Kratos, I created a Guard that depends on OryFrontendService to fetch and validate the session from the cookie or session token. The Guard is a mixin allowing different configurations for each HTTP endpoint or microservice listener.

// from https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/kratos-client-wrapper/src/lib/ory-authentication.guard.ts
// ...
export const OryAuthenticationGuard = (
  options: Partial<OryAuthenticationGuardOptions> = defaultOptions
): Type<CanActivate> => {
  @Injectable()
  class AuthenticationGuard implements CanActivate {
    readonly logger = new Logger(AuthenticationGuard.name);

    constructor(readonly oryService: OryFrontendService) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
      const {
        cookieResolver,
        sessionTokenResolver,
        isValidSession,
        postValidationHook,
      } = {
        ...defaultOptions,
        ...options,
      };

      try {
        const cookie = cookieResolver(context);
        const xSessionToken = sessionTokenResolver(context);
        const { data: session } = await this.oryService.toSession({
          cookie,
          xSessionToken,
        });
        if (!isValidSession(session)) {
          return false;
        }
        if (typeof postValidationHook === 'function') {
          await postValidationHook.bind(this)(context, session);
        }
        return true;
      } catch (error) {
        this.logger.error(error);
        return false;
      }
    }
  }
  return mixin(AuthenticationGuard);
};
Enter fullscreen mode Exit fullscreen mode

Add unit tests

I wanted to be sure that the Ory clients use the custom Axios instance with the options provided by the OryBaseModule, so I created a rather minimalist unit tests suite for both services, which mock the OryBaseService and, by extension, the Axios instance.

import { OryBaseService } from '@getlarge/base-client-wrapper';
import { Test, TestingModule } from '@nestjs/testing';

import { OryIdentitiesModuleOptions } from './ory-identities.interfaces';
import { OryIdentitiesService } from './ory-identities.service';

describe('OryIdentitiesService', () => {
  let oryIdentitiesService: OryIdentitiesService;
  let oryBaseService: OryBaseService;
  const accessToken = 'ory_st_test';
  const dummyResponse = { data: 'test' };

  beforeEach(async () => {
    // the Test module is used to create a NestJS context with the provider we want to test and its dependencies
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OryIdentitiesService,
        {
          provide: OryIdentitiesModuleOptions,
          useValue: {
            basePath: 'http://localhost',
            accessToken,
          },
        },
        // OryBaseService is mocked to avoid making actual HTTP requests and to spy on the axios instance
        {
          provide: OryBaseService,
          useValue: {
            axios: {
              request: jest.fn().mockResolvedValue(dummyResponse),
            },
          },
        },
      ],
    }).compile();

    oryIdentitiesService =
      module.get<OryIdentitiesService>(OryIdentitiesService);
    oryBaseService = module.get<OryBaseService>(OryBaseService);
  });

  it('should be defined', () => {
    expect(oryIdentitiesService).toBeDefined();
  });

  describe('Should use custom axios instance', () => {
    it('should call getIdentity ednpoint', async () => {
      const id = 'test';

      await oryIdentitiesService.getIdentity({ id });
      // when calling oryIdentitiesService method, the injected OryBaseService axios instance should be called
      expect(oryBaseService.axios.request).toHaveBeenCalledWith({
        url: `http://localhost/admin/identities/${id}`,
        method: 'GET',
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      });
    });

    //...
  });
});
Enter fullscreen mode Exit fullscreen mode

keto-relations-parser implementation

As said earlier, the original goal was to improve the original library that allows parsing relation tuples with the following changes:

  1. Replace Antlr4 based parser with a simpler and more efficient Regex parser (it still needs to be tested in some edge cases)
  2. Create a fluent API to construct a relation tuple object
  3. Provide a set of helpers to convert RelationTuple to Ory Keto API parameters

After a quick discussion with the author of the original library, it seemed like the Antlr4 parser was a bit overkill for the use case and that a more straightforward Regex match could be enough. The first task was to write a Regex that matched the Ory Permission Language notation.
Here are the test cases I used to validate the Regex:

const validCases = [
  'namespace:object#relation@subject',
  'namespace:object#relation@(subject)',
  'namespace:object#relation@subjectNamespace:subjectObject#subjectRelation',
  'namespace:object#relation@(subjectNamespace:subjectObject#subjectRelation)',
  'namespace:object#relation@(subjectNamespace:subjectObject)',
  'namespace:object#relation@(subjectNamespace:subjectObject#)',
  'namespace:object#relation@(subjectNamespace:subjectObject#subjectRelation)',
];

const invalidSyntaxtTestCases = [
  'object#relation',
  'object@subject',
  'object#relation@subject@sdf',
  'object#relation@subjectObject#relation@sdf',
  'object#relation@subjectObject#relation',
  'namespace:object#relation@subjectObject#relation',
  'object#relation@namespace:subjectObject#relation',
  'object#relation@subjectId',
  'namespace:object#@subjectId',
  'namespace:#relation@subjectId',
  ':object#relation@subjectId',
  'namespace:object#relation@',
  'namespace:object#relation@:subjectObject#relation',
  'namespace:object#relation@namespace:#relation',
  'namespace:object#relation@id:',
  'namespace::object#relation@id',
  'namespace:object##relation@id',
  'namespace:object#relation@@id',
];
Enter fullscreen mode Exit fullscreen mode

And the Regex is brought to you by OpenAI:

/^([^:]+)(?::([^#]+))?(?:#([^@]+)(?:@([^:]+)(?::([^#]+))?(?:#([^()]+(?:\([^()]+\))?)?)?)?)?$/
Enter fullscreen mode Exit fullscreen mode

Please let me know if there is a better way to write this Regex!


Another discussion with some co-workers made me realize the importance of providing a fluent API to construct the relation tuple. Using the notation supplied by the Ory Permission Language, it was hard to verbalize the relationships.
This results in the following API:

import { RelationTupleBuilder } from '@getlarge/keto-relations-parser';

const tuple = new RelationTupleBuilder()
  .subject('User', '321')
  .isIn('owners')
  .of('Project', '321');

tuple.toString(); // 'Project:321#owners@User:321'

tuple.toJSON(); // { namespace: 'Project', id: '321', relation: 'owners', subject: { namespace: 'User', id: '321' } }

tuple.toHumanReadableString(); // 'User:321 is in owners of Project:321'
Enter fullscreen mode Exit fullscreen mode

Finally, I provided a set of helpers to convert the RelationTuple to Ory Keto API parameters; this API seems to be a bit inconsistent in the way it expects the parameters, so I tried hiding it by making the RelationTuple interface the single model to interact with.

You can find those helpers here.

keto-client-wrapper implementation

The approach is the same as for the kratos-client-wrapper implementation, except that the modules are named OryPermissionsModule and OryRelationshipsModule.

Create an authorization guard

For this Guard case, things are more complicated than the OryAuthenticationGuard as multiple layers of abstraction are required to empower developers to define permissions on their endpoints.
Hopefully, the expressiveness of the Ory Permission Language combined with the fluent API from keto-relations-parser allows us to check permissions elegantly.

Note
Want to know more about this notation and how it works? Check out the Zanzibar Academy.

Create OryPermissionChecks decorator

The relation tuple(s) must be dynamically constructed with the API endpoint parameters and passed to OryPermissionService to check permissions on Ory Keto. Ideally, the Guard should be able to handle multiple relation tuples and multiple checks, so the Guard should receive an array of relation tuples factories.
Also, it would be ideal to define those factories declaratively, so I decided to use a custom decorator, OryPermissionChecks. The factories are resolved using Reflection and the Execution Context.

import { ExecutionContext, SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

const ORY_PERMISSION_CHECKS_METADATA_KEY = Symbol('OryPermissionChecksKey');

export type RelationTupleFactory = string | ((ctx: ExecutionContext) => string);

export type RelationTupleCondition = {
  type: 'AND' | 'OR';
  conditions: (RelationTupleFactory | RelationTupleCondition)[];
};

export type EnhancedRelationTupleFactory =
  | RelationTupleFactory
  | RelationTupleCondition;

/**
 * @description Decorator to add permission checks to a handler, will be consumed by the `OryAuthorizationGuard` {@link OryAuthorizationGuard} using the `getOryPermissionChecks` {@link getOryPermissionChecks} function
 * @param relationTupleFactories
 * @returns
 */
export const OryPermissionChecks = (
  ...relationTupleFactories: EnhancedRelationTupleFactory[]
) => {
  return SetMetadata(
    ORY_PERMISSION_CHECKS_METADATA_KEY,
    relationTupleFactories
  );
};

export const getOryPermissionChecks = (
  reflector: Reflector,
  handler: Parameters<Reflector['get']>[1]
): EnhancedRelationTupleFactory[] | null => {
  const oryPermissions =
    reflector.get<
      EnhancedRelationTupleFactory[],
      typeof ORY_PERMISSION_CHECKS_METADATA_KEY
    >(ORY_PERMISSION_CHECKS_METADATA_KEY, handler) ?? [];
  return oryPermissions.length > 0 ? oryPermissions : null;
};
Enter fullscreen mode Exit fullscreen mode

Note
The permissions to checks can be composed of multiple relation tuples and multiple conditions. The conditions can be nested and combined with AND or OR logical operators. This allows for a high level of expressiveness when defining permissions.

Create OryAuthorizationGuard

The Guard is a mixin that depends on:

  • OryPermissionChecks, this decorator enables to store the relation tuple(s) that represents the permission(s) required to access the given endpoint
  • OryPermissionService, check the permission(s) via Ory Keto HTTP API.

The most important part of the Guard is the evaluateConditions method, which resolves the relation tuple(s) and checks the permissions via the Ory Keto API. The method is recursive and can handle nested conditions.

// from https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/keto-client-wrapper/src/lib/ory-authorization.guard.ts
// ...
export const OryAuthorizationGuard = (
  options: Partial<OryAuthorizationGuardOptions> = {}
): Type<IAuthorizationGuard> => {
  @Injectable()
  class AuthorizationGuard implements CanActivate {
    constructor(
      readonly reflector: Reflector,
      readonly oryService: OryPermissionsService
    ) {}

    get options(): OryAuthorizationGuardOptions {
      return {
        ...defaultOptions,
        ...options,
      };
    }

    async evaluateConditions(
      factory: EnhancedRelationTupleFactory,
      context: ExecutionContext
    ): Promise<{
      allowed: boolean;
      relationTuple: string | string[];
    }> {
      if (typeof factory === 'string' || typeof factory === 'function') {
        const { unauthorizedFactory } = this.options;

        const relationTuple =
          typeof factory === 'string' ? factory : factory(context);
        const result = createPermissionCheckQuery(
          parseRelationTuple(relationTuple).unwrapOrThrow()
        );

        if (result.hasError()) {
          throw unauthorizedFactory(context, result.error);
        }

        try {
          const { data } = await this.oryService.checkPermission(result.value);
          return { allowed: data.allowed, relationTuple };
        } catch (error) {
          throw unauthorizedFactory(context, error);
        }
      }
      const evaluatedConditions = await Promise.all(
        factory.conditions.map((cond) => this.evaluateConditions(cond, context))
      );
      const results = evaluatedConditions.flatMap(({ allowed }) => allowed);
      const allowed =
        factory.type === 'AND' ? results.every(Boolean) : results.some(Boolean);

      return {
        allowed,
        relationTuple: evaluatedConditions.flatMap(
          ({ relationTuple }) => relationTuple
        ),
      };
    }

    async canActivate(context: ExecutionContext): Promise<boolean> {
      const factories =
        getOryPermissionChecks(this.reflector, context.getHandler()) ?? [];
      if (!factories?.length) {
        return true;
      }
      const { postCheck, unauthorizedFactory } = this.options;
      for (const factory of factories) {
        const { allowed, relationTuple } = await this.evaluateConditions(
          factory,
          context
        );

        if (postCheck) {
          postCheck(relationTuple, allowed);
        }
        if (!allowed) {
          throw unauthorizedFactory(
            context,
            new Error(`Unauthorized access for ${relationTuple}`)
          );
        }
      }
      return true;
    }
  }
  return mixin(AuthorizationGuard);
};
Enter fullscreen mode Exit fullscreen mode

Note

The unit tests for the Guard here, can give you a better understanding of how it works.

End2end testing

To ensure that the libraries work well together and that the Guard is successfully activated to protect endpoints, I created the following setup:

  • Custom Docker images for Kratos and Keto
  • A mocked application that uses the libraries
  • Some end-to-end tests using NestJS testing utilities and Jest

The setup refers to the keto-client-wrapper, but the same applies to the kratos-client-wrapper.

Build custom docker images

I created a Dockerfile for each service. The Dockerfile for Keto is simple; it copies the configuration and the namespaces files, and sets the health check and the command to run the service.

FROM oryd/keto:v0.11.1

COPY ./keto.yaml /home/ory/keto.yaml
COPY ./namespaces.ts /home/ory/namespaces.ts

HEALTHCHECK --interval=3s --timeout=3s --start-period=2s --retries=3 CMD [ 'wget -nv --spider -t1 http://localhost:4466/health/ready || exit 1' ]

CMD ["serve", "--config", "/home/ory/keto.yaml"]
Enter fullscreen mode Exit fullscreen mode

Note
The health check will help us ensure the container is ready to accept requests before running tests

Then, we can add an Nx target to make publishing to GHCR easy:

// ...
"docker-push": {
  "executor": "nx:run-commands",
  "options": {
    "command": "docker buildx build ./test --platform linux/amd64,linux/arm64 -t ghcr.io/getlarge/nestjs-ory-integration/keto --push",
    "cwd": "packages/keto-client-wrapper"
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, the image is published with nx run keto-client-wrapper:docker-push.

Mock an application

Here is a simple Controller that interacts with the library and provides an endpoint that returns a 200 if the user has the required permissions to access the resource or a 403 if not.

@Controller('Example')
export class ExampleController {
  @OryPermissionChecks((ctx) => {
    const req = ctx.switchToHttp().getRequest();
    const currentUserId = req.headers['x-current-user-id'] as string;
    const resourceId = req.params.id;
    return new RelationTupleBuilder()
      .subject('User', currentUserId)
      .isIn('owners')
      .of('Toy', resourceId)
      .toString();
  })
  @UseGuards(
    OryAuthorizationGuard({
      postCheck(relationTuple, isPermitted) {
        Logger.log('relationTuple', relationTuple);
        Logger.log('isPermitted', isPermitted);
      },
    })
  )
  @Get(':id')
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  getExample(@Param('id') id?: string) {
    return {
      message: 'OK',
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Write e2e tests

In this test, I create a relation (owners) between a subject User and a resource Toy in Ory Keto and check if the user can access the resource via the endpoint.
The Keto policies are defined in namespaces.ts.

// from https://github.com/getlarge/nestjs-ory-integration/blob/main/packages/keto-client-wrapper/test/keto-client-wrapper.spec.ts

describe('Keto client wrapper E2E', () => {
  let app: INestApplication;
  let oryPermissionService: OryPermissionsService;
  let oryRelationshipsService: OryRelationshipsService;
  const dockerComposeFile = resolve(join(__dirname, 'docker-compose.yaml'));

  const route = '/Example';

  const createOryRelation = async (object: string, subjectObject: string) => {
    const relationTuple = relationTupleBuilder()
      .subject('User', subjectObject)
      .isIn('owners')
      .of('Toy', object)
      .toJSON();
    await oryRelationshipsService.createRelationship({
      createRelationshipBody:
        createRelationQuery(relationTuple).unwrapOrThrow(),
    });
    const { data } = await oryPermissionService.checkPermission(
      createPermissionCheckQuery(relationTuple).unwrapOrThrow()
    );
    expect(data.allowed).toEqual(true);
  };

  beforeAll(() => {
    if (!process.env['CI']) {
      execSync(`docker-compose -f ${dockerComposeFile} up -d --wait`, {
        stdio: 'ignore',
      });
    }
  });

  afterAll(() => {
    if (!process.env['CI']) {
      execSync(`docker-compose -f ${dockerComposeFile} down`, {
        stdio: 'ignore',
      });
    }
  });

  beforeEach(async () => {
    // before each test, we create a NestJS context with the modules we want to test and their dependencies
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        // we ensure that loading modules with both forRoot and forRootAsync methods works
        OryPermissionsModule.forRoot({
          basePath: 'http://localhost:44660',
        }),
        OryRelationshipsModule.forRootAsync({
          useFactory: () => ({
            basePath: 'http://localhost:44670',
            accessToken: '',
          }),
        }),
      ],
      providers: [ExampleService],
      controllers: [ExampleController],
    }).compile();

    oryPermissionService = module.get<OryPermissionsService>(
      OryPermissionsService
    );
    oryRelationshipsService = module.get<OryRelationshipsService>(
      OryRelationshipsService
    );

    app = module.createNestApplication();
    await app.init();
  });

  afterEach(() => {
    return app?.close();
  });

  it('should pass authorization when relation exists in Ory Keto', async () => {
    const object = 'car';
    const subjectObject = 'Bob';
    await createOryRelation(object, subjectObject);

    const { body } = await request(app.getHttpServer())
      .get(`${route}/${object}`)
      .set({
        'x-current-user-id': subjectObject,
      });
    expect(body).toEqual({ message: 'OK' });
  });

  it('should fail authorization when relation does not exist in Ory Keto', async () => {
    const object = 'car';
    const subjectObject = 'Alice';

    const { body } = await request(app.getHttpServer())
      .get(`${route}/${object}`)
      .set({
        'x-current-user-id': subjectObject,
      });
    expect(body).toEqual({
      error: 'Forbidden',
      message: 'Forbidden resource',
      statusCode: 403,
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Note
Before starting the tests and when running locally, Ory services are started with docker compose with the wait option to ensure services are up and running and stopped after the tests are done. In CI, the services are already running, so the tests can be run directly.

Conclusion

I hope this article will offer a better understanding of Ory APIs and concrete interactions with NestJS. The libraries are available on GitHub.

In this series's next article, I will show you how to integrate the libraries into a NestJS application and configure Ory Kratos and Ory Keto in a production environment. Stay tuned! πŸš€

Top comments (0)