DEV Community

Niclas
Niclas

Posted on

Using Ory Keto to secure NestJS backends

Ory Keto is a Permissions & Access Control System based on Zanzibar: Google’s Consistent, Global Authorization System.
It uses a graph to evaluate permissions and is therefore very flexible in its structure.
Allowed access is nothing more than an existing path in the graph.

For checking permissions it uses a structure called Relation Tuple.
The string-representation has the following syntax (BNF):

<relation-tuple> ::= <object>'#'relation'@'<subject>
<object> ::= namespace':'object_id
<subject> ::= subject_id | <subject_set>
<subject_set> ::= <object>'#'relation
Enter fullscreen mode Exit fullscreen mode

A valid example would be namespace:object#relation@subjectId or namespace:object#relation@subjectNamespace:subjectObject#subjectRelation.
The latter can be difficult to read, therefore you'll often see the following variant: namespace:object#relation@(subjectNamespace:subjectObject#subjectRelation)

Relation Tuples can be verbalized.
The Relation Tuple sharedFiles:a.txt#access@dirs:b#access can be verbalized as "Anyone with access to dirs:b has access to sharedFiles:a.txt".

The SDKs of Ory Keto (REST and gRPC) use an object/class representation of the Relation Tuple. This is fine for most cases but not as convenient to use and understand as a simple string.

Because of this I wrote a small Typescript library to parse the string-representation of a Relation Tuple into the needed object-representation.

The general variant, not tied to Keto, is called @nidomiro/relation-tuple-parser (npm, github).
It supports converting static strings and strings with placeholders.
The latter is especially useful if you want to substitute e.g. the id of the currently logged-in user in the Relation Tuple.

The variant for Keto @nidomiro/relation-tuple-parser-ory-keto (npm, GitHub) just contains converters. They convert the given Relation Tuple (with, or without replacements) into a form you can use with the Keto SDK.

Let's look into a usage-example.

Example with NestJS

I assume you already know NestJS and are in a freshly created project. Additionally, I assume that Keto is already accessible.

The full source-code of this example can be found here

What we want to accomplish

  • We want an easy way to secure a backend route with Keto.
  • The person developing a new route shouldn't be responsible for communication with Keto.

The routes permissions should therefore be configurable via a Decorator. The choice fell to the following syntax:

@GuardedBy(({ currentUserId }) => `groups:users#member@${currentUserId}`)
Enter fullscreen mode Exit fullscreen mode

It is concise and flexible at the same time.
We can use it, to secure our getData route:

@Controller()
export class AppController {
    @GuardedBy(({ currentUserId }) => `groups:users#member@${currentUserId}`)
    @Get()
    getData() {
        return 'Access grated!'
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing the Decorator

How is the decorator shown above implemented?

In fact, it is relatively simple. It accepts a string or a generator function as parameter. Then it parses the given Relation Tuples and stores them inside the Handlers' metadata.
This way, the parsing happens at app-startup and not with every request.

The decorator (full source):

export const GuardedBy = (relationTuple: string | RelationTupleStringGenerator<PossibleReplacements>) => {
    let valueToSet: GuardedByMetadataType
    if (typeof relationTuple === 'string') {
        valueToSet = parseRelationTupleWithReplacements(() => relationTuple).unwrapOrThrow()
    } else {
        valueToSet = parseRelationTupleWithReplacements(relationTuple).unwrapOrThrow()
    }

    return SetMetadata(GUARDED_BY_METADATA_KEY, valueToSet)
}
Enter fullscreen mode Exit fullscreen mode

Note that both variants are stored as a RelationTupleWithReplacements. This way we only have to think about one variant during the evaluation.

To statically type the possible replacements we create a type:

export type PossibleReplacements = {
    currentUserId: string | 'Unauthorized'
}
Enter fullscreen mode Exit fullscreen mode

Implementing the (App-) Guard

The actual check of the Relation Tuple against Keto happens inside the Guard - KetoGuard (full source) in our case.

First we need to add the Keto SDK: npm i @ory/keto-grpc-client and if you need the REST-client npm i @ory/keto-client.

We use the gRPC variant for checking, because it is a bit faster than the HTTP variant - at least in theory.

What we need to do in the Guard:

  1. get the value set by the Guard (the RelationTupleWithReplacements)
  2. get the needed replacement values. In our case just currentUserId.
  3. replace the values in the RelationTuple and evaluate the tuple against Keto
  4. Allow / Deny access according to the result of the check.

For step 1 create another method inside guarded-by.decorator.ts (full source) with the following definition:

export const getGuardingRelationTuple = (
    reflector: Reflector,
    handler: Parameters<Reflector['get']>[1],
): RelationTupleWithReplacements<PossibleReplacements> | null => {
    return (
        reflector.get<GuardedByMetadataType, typeof GUARDED_BY_METADATA_KEY>(GUARDED_BY_METADATA_KEY, handler) ?? null
    )
}
Enter fullscreen mode Exit fullscreen mode

Now we can retrieve the RelationTupleWithReplacements if any is set. Otherwise, we get null instead.

For step 2 I chose the "simple demo approach" and just extract a userId from the URL parameters. Only use this approach for demos, never for production! (source code)

Step 3 is implemented in keto-read-client.service.ts (full source).
With RelationTupleWithReplacementsConverter.toKetoGrpcCheckRequest we replace the values inside the RelationTuple and store the result directly in an CheckRequest, needed by Keto's gRPC client.
The next step is to call the Keto client with our CheckRequest and return the result. Note that the error-status NOT_FOUND is handled as "not allowed" (see here).

The last and final step is to use the Result and allow or deny the Request.

Now we need to load some rules into keto and check the result.
For this demo the necessary rules are set in keto during app-startup (full-source):

async function initKeto(ketoWriteService: KetoWriteClientService) {
    await ketoWriteService.addRelationTuple(parseRelationTuple('groups:users#member@user1').unwrapOrThrow())
    await ketoWriteService.addRelationTuple(parseRelationTuple('groups:users#member@user2').unwrapOrThrow())
}
Enter fullscreen mode Exit fullscreen mode

With these Relation Tuples set, you can now check the result yourself:

Allowed Access returns:

Access grated!
Enter fullscreen mode Exit fullscreen mode

Denied Access returns:

{"statusCode":403,"message":"Forbidden resource","error":"Forbidden"}
Enter fullscreen mode Exit fullscreen mode

And that's it. Now you can add more Controller and/or Handler and just use the @GuardedBy decorator to secure them.

In a real world-example you would also validate the session and fetch the data of the currentUserId inside the guard. The fetched data can then be attached to the request and therefore be used by the Handler without the need to fetch the data again.

If you have any questions or improvements feel free to comment below.

Top comments (0)