The subject represents the current user as well as any of their authorization-related data, such as permissions and memberships.
The current user is provided using the getSubject
adapter to allow getting the subject from any system or authentication provider.
Defining a getSubject
adapter
Implement your getSubject
adapter by passing it to createKilpi
. It should call your authentication system or provider and return any value that represents the current subject or e.g. null
if there is no authenticated subject.
export const Kilpi = createKilpi({ async getSubject() { const user = await myAuthenticationProvider.getCurrentUser(); if (!user) return null; return { id: user.id, name: user.name }; }, // ...});
The subject
’s type is automatically inferred by your policies
.
Passing a context argument
Commonly, your getSubject
may require additional context (e.g. the current request
).
export const Kilpi = createKilpi({ async getSubject(ctx?: Request) { if (!ctx) return null; const user = await myAuthenticationProvider.getCurrentUser(ctx); if (!user) return null; return { id: user.id, name: user.name }; }, // ...});
You can then pass this ctx
object when authorizing using the authorize()
API.
await Kilpi.some.policy().authorize({ ctx: request });
Below is an example implementation using a global middleware to provide the ctx
.
import { AsyncLocalStorage } from "async_hooks";
// Create an AsyncLocalStorage which can provide the `ctx`export const ctxStorage = new AsyncLocalStorage<Context>();
// Provide the `ctx` in a middleware -- all code running inside// this block has access to `const ctx = ctxStorage.getStore()`.app.use(async (ctx, next) => { await ctxStorage.run(ctx, async () => { await next(); });});
// Later getSubject can use the ctxStorage to consume the ctxasync function getSubject() { const ctx = ctxStorage.getStore(); if (!ctx) return null;
// ...}
Additional data
Very commonly, your subject has other authorization-related properties not received directly from your authentication provider. These include e.g. permissions, roles, and memberships. It is often practical and efficient to include them in your getSubject
instead of fetching in policies.
export const Kilpi = createKilpi({ async getSubject(ctx?: Request) { const user = await myAuthenticationProvider.getCurrentUser(ctx); if (!user) return null; const memberships = await db.listMembershipsForUser(user); return { ...user, memberships }; }, // ...});
Accessing the subject
To access the subject, you can get it from the result of authorize()
when the decision is granted (or when using .assert()
) or by using the Kilpi.$getSubject()
utility.
const subject = await Kilpi.$getSubject();
Performance considerations and caching / deduping
Kilpi calls getSubject
for you on every single authorize()
call. Especially, if your getSubject
is an expensive or slow function, you should consider caching it for each request (deduplicating).
This can be done in multiple ways, and some plugins (such as ReactServerPlugin
) even automatically cache the subject for each request.
If your runtime supports async_hooks
and you have access to your application’s entrypoints (such as with a global middleware), you can use the following solution based on AsyncLocalStorage
.
import { AsyncLocalStorage } from "async_hooks";
// Create AsyncLocalStorage for providing the cached subjectconst subjectStorage = new AsyncLocalStorage<{ value?: { subject: Subject };}>();
// Store subject in AsyncLocalStorageKilpi.$hooks.onSubjectResolved((event) => { const store = subjectStorage.getStore(); if (store) store.value = { subject: event.subject };});
// Inject subject from cache before `getSubject` is calledKilpi.$hooks.onSubjectRequestFromCache(() => { const store = subjectStorage.getStore(); return store.value;});
// Wrap your application entrypoint with the AsyncLocalStorage, e.g.// using a middleware or similar solutionapp.use(async (ctx, next) => { await subjectStorage.run({}, async () => { await next(); });});
You might in some contexts with a mutable request
object be able to store the subject directly in the request.
type SubjectCache = { subjectCache?: { subject: Subject } };
async function getSubject( ctx?: Request & { subjectCache?: { subject: Subject }; },) { if (!ctx) return null;
// Cache hit if (ctx.subjectCache) return ctx.subjectCache.subject;
// Cache miss: Get subject and store it in cache const subject = await getCurrentUser(ctx); ctx.subjectCache = { subject }; return subject;}