Authorizing

All you need to know about authorizing with Kilpi's authorize() and assert() APIs.


All server-side authorizations happen with the authorize() API.

Authorization using authorize()

Reference a policy as e.g. Kilpi.posts.edit(post) and call the policy’s authorize() method. This evaluates the policy and returns an authorization decision. In many cases, this API is all you need.

const decision = await Kilpi.posts.edit(post).authorize();
if (decision.granted) {
await db.posts.update(...);
}

The decision is either a granted or a denied decision.

type Decision<TSubject> =
| {
granted: true; // Signals "granted decision" (Discriminator)
subject: TSubject; // The subject from the policy's Grant(subject)
}
| {
granted: false; // Signals "denied decision" (Discriminator)
message?: string; // User-facing message, e.g. "You are not signed in"
reason?: string; // System-facing reason, e.g. "UNAUTHORIZED"
metadata?: unknown; // Optional metadata about the denial
};

Providing a context

If your getSubject adapter requires a ctx parameter (e.g. the current Request), you can pass it as follows.

await Kilpi.myPolicy().authorize({ ctx });

Throw on unauthorized with assert()

In very many cases, you want to throw on unauthorized, especially when your framework supports throwing e.g. HTTP errors or redirections. For this purpose, you can use the assert() API as follows.

// Always returns a granted decision (or throws)
const { subject } = await Kilpi.myPolicy().authorize().assert();

When on onUnauthorizedAssert handlers are defined, Kilpi throws a KilpiUnauthorizedError. To customize what is thrown (or to run side effects), you can provide a global onUnauthorizedAssert handler.

export const Kilpi = createKilpi({
// ...
// Called when `.assert()` denies access.
async onUnauthorizedAssert(denial) {
console.log(`Denied: ${denial.message}`);
throw new HttpForbiddenError();
},
});

To override the default behavior, you can also provide an onUnauthorizedAssert callback to the assertion to run before the global handler.

await Kilpi.usePremiumFeature()
.authorize()
.assert(async (denial) => {
throw HttpRedirect(`/subscribe?message=${denial.message}`);
});

Handling different types of denials

The Deny() function can provide additional data about the reason of the denial. This data can then be used anywhere you have the denied decision, such as in the global onUnauthorizedAssert handler as shown below.

const Kilpi = createKilpi({
// ...
policies: {
posts: {
// Only signed-in premium users can save posts
async save(subject, post: Post) {
if (!subject) {
return Deny({ reason: "UNAUTHENTICATED" });
}
if (subject.tier !== "premium") {
return Deny({
reason: "NOT_SUBSCRIBED",
metadata: { requiredTier: "premium" },
});
}
return Grant(subject);
},
},
},
// Customize behavior based on reason
async onUnauthorizedAssert(denial) {
switch (denial.reason) {
case "UNAUTHENTICATED":
throw new HttpRedirect("/sign-in");
case "NOT_SUBSCRIBED":
const tier = denial.metadata?.requiredTier;
throw new HttpRedirect(`/subscribe?tier=${tier}`);
default:
throw new HttpForbiddenError();
}
},
});