Introduction
Kilpi [/ˈkilpi/] is the Finnish word for shield.
What is Kilpi
Kilpi is an authorization framework for implementing a robust authorization layer in your application.
Philosophy and design principles
Kilpi is an opinionated library designed based on certain principles. Read through these decisions to see, whether Kilpi suits your authorization requirements.
-
Server-first authorization
All authorization checks are evaluated on the server for security, making Kilpi well suited for server-side applications (Express, Next.js, …).
-
Centralized authorization layer
Kilpi is designed around a centralized authorization layer consisting of a set of policies, referred to by their keys, such as
documents:update
. -
Declarative
All Kilpi APIs are designed to be declarative, from Policies to Protected Queries making for a simpler authorization model.
-
Policies as code
Policies are defined as code instead of using a no-code interface or a domain-specific language. This ensures type safety, an easy learning curve, and version controlled policies.
-
Asynchronous policies
Policies can be asynchronous and fetch data during evaluation. This makes them more flexible and powerful, and reduces data fetching boilerplate in your code.
-
Framework agnostic
Kilpi is designed to be framework agnostic and can be applied to any technology (with or without plugins). See the installation guides for how to make Kilpi work with your framework of choice.
-
Authentication provider agnostic
Kilpi is designed around the concept of a subject, which can be used to wrap any auth provider or data source. This means you can even change authentication providers without changing your authorization layer.
-
Authorization model agnostic
Kilpi does not implement any advanced authorization concepts (roles, permissions, organizations, memberships, …), nor does it enforce any single implementation. Instead, Kilpi is designed to be flexible enough to facilitate any authorization model, such as custom permissions, ABAC, RBAC and ReBAC using Kilpi.
Who is Kilpi not designed for?
Kilpi may not suit you, if you…
- Do not work in a full TypeScript (or JavaScript) project.
- Do not require authorization.
- Do not want a centralized authorization layer.
- Require a ready-made no-code interface or a domain-specific language for policies.
- Require an authorization model that provides its own implementation for roles, permissions, etc.
Motivation
I’ve built over a dozen applications throughout my career. And I’ve built authorization into them time after time. And I’ve created half-baked abstractions for authorization way too often trying to refactor countless if (user.role === "admin" && ...)
statements littered throughout my pages, mutations, queries and UI components.
This has made maintenance troublesome, bug-prone and time-consuming, when new features are added or the authorization logic requires changing.
I knew I wasn’t alone.
Kilpi is an attempt to solve this problem once and for all. It aims to be a generic solution to suit all use cases, no matter your authorization needs.b
No expensive lock-in
Many paid authentication services (Clerk, Auth0, Kinde, …) also offer their own fine-grained authorization solutions.
However, they can be problematic as they lock you in to their product, migrating out of which can be difficult and expensive.
The benefit of using an explicit authorization layer, such as Kilpi, is that you are not locked in. You can use any of these authentication providers and even implement policies using their products, but you are much less locked in.
Addressing OWASP top ten security risks
The OWASP Top Ten lists the top 10 security risks for web applications. This library helps you address two of them related to authorization.
OWASP A01:2021
: Broken Access Control (Listed at #1)OWASP A04:2021
: Insecure Design (Listed at #4)
Kilpi offers you a secure design for your authorization and by centralizing and making your policies explicit, it helps you avoid broken access control, especially when policies change.
Show me the code
Setting up Kilpi and defining your first policy.
export const Kilpi = createKilpi({ getSubject, policies: { documents: { update(user, doc: Document) { if (!user) return deny("Unauthenticated"); return user.id === doc.ownerId ? grant(user) : deny(); } } }})
Protecting actions, mutations and functions
// Protect an action, mutation or functionfunction updateDocument(id: string) { const doc = await db.getDocument(id); await Kilpi.authorize("documents:update", doc); // Throws if fails await db.updateDocument(doc);}
Protecting queries
const getDocument = Kilpi.query( async (id: string) => await db.getDocument(id), { // When calling via `protect`, the output is passed through the protector async protector({ output: doc }) { if (doc) await Kilpi.authorize("documents:read", doc); return doc; } })
const authorizedDocument = await getDocument.protect("1");const unauthorizedDocument = await getDocument.unsafe("2"); // Skips protectorgetDocument("1") // Error -- must use protect or unsafe
Protecting pages and UI
export default async function Page(props) { // Handle what happens when any auth check fails and throws Kilpi.onUnauthorized(() => redirect("/"));
// Ensure user is authed AND has access to the document await Kilpi.authorize("authed"); const doc = await getDocument.protect(props.id);
return ( <main> <h1>{doc.title}</h1>
<Access to="documents:update" on={doc} Unauthorized={<p>Not allowed to edit this document</p>} Loading={<p>Loading...</p>} > <button>Edit document</button> </Access> </main> );}