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
andkratos-cli
, to interact with the Ory APIs from the command line andhydra-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:
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
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;
}
//...
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 {}
}
//...
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;
}
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);
}
);
}
}
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 theHttpModule
and provided theOryBaseService
with theHttpService
instance - With the
forRootAsync
method, the options are provided asynchronously, so instead of importing theHttpModule
, I created a customHttpService
provider that receives an Axios instance to its constructor, configured with theOryBaseModuleOptions
.
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);
};
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}`,
},
});
});
//...
});
});
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:
- Replace Antlr4 based parser with a simpler and more efficient Regex parser (it still needs to be tested in some edge cases)
- Create a fluent API to construct a relation tuple object
- 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',
];
And the Regex is brought to you by OpenAI:
/^([^:]+)(?::([^#]+))?(?:#([^@]+)(?:@([^:]+)(?::([^#]+))?(?:#([^()]+(?:\([^()]+\))?)?)?)?)?$/
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'
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;
};
Note
The permissions to checks can be composed of multiple relation tuples and multiple conditions. The conditions can be nested and combined withAND
orOR
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);
};
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"]
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"
}
}
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',
};
}
}
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,
});
});
});
Note
Before starting the tests and when running locally, Ory services are started withdocker compose
with thewait
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)