diff --git a/versioned_docs/version-3.x/recipe/auth-integration/better-auth.md b/versioned_docs/version-3.x/recipe/auth-integration/better-auth.md index 4a7debe7..8494d500 100644 --- a/versioned_docs/version-3.x/recipe/auth-integration/better-auth.md +++ b/versioned_docs/version-3.x/recipe/auth-integration/better-auth.md @@ -1,5 +1,6 @@ --- sidebar_position: 1 +sidebar_label: Better-Auth --- import PackageInstall from '../../_components/PackageInstall'; @@ -25,7 +26,9 @@ Add the adapter to your better-auth configuration: ```ts import { zenstackAdapter } from '@zenstackhq/better-auth'; -import { db } from './db'; // your ZenStack ORM client + +// ZenStack ORM client +import { db } from './db'; const auth = new BetterAuth({ database: zenstackAdapter(db, { @@ -45,6 +48,8 @@ Then, run the "generate" command to generate the schema: +You should see models like `User`, `Session`, and `Account` added to your `schema.zmodel` file if they don't already exist. + Alternatively, you can refer to [better-auth schema documentation](https://www.better-auth.com/docs/concepts/database#core-schema) to manually add the necessary models. After the schema is configured, you can then use the regular ZenStack database schema migration workflow to push the schema to your database. @@ -77,7 +82,10 @@ const userId = session.userId; Then you can pass it to `ZenStackClient`'s `$setAuth()` method to get a user-bound ORM client. ```tsx -const userDb = db.$setAuth({ userId }); +// ZenStack ORM client with access policy plugin installed +import { authDb } from './db'; + +const userDb = authDb.$setAuth({ userId }); ``` ### Organization plugin support @@ -109,7 +117,7 @@ After enabling the Organization plugin and running the CLI to generate the addit Then you can use the full `userContext` object to get a user-bound client. ```tsx -const userDb = db.$setAuth(userContext); +const userDb = authDb.$setAuth(userContext); ``` The user context will be accessible in ZModel policy rules via the special `auth()` function. To get it to work, let's add a type in ZModel to define the shape of `auth()`: diff --git a/versioned_docs/version-3.x/recipe/auth-integration/clerk.md b/versioned_docs/version-3.x/recipe/auth-integration/clerk.md new file mode 100644 index 00000000..8e04c9a0 --- /dev/null +++ b/versioned_docs/version-3.x/recipe/auth-integration/clerk.md @@ -0,0 +1,68 @@ +--- +description: Integrating with Clerk. +sidebar_position: 2 +sidebar_label: Clerk +--- + +# Clerk Integration + +[Clerk](https://clerk.com/) is a comprehensive authentication and user management platform, providing both APIs and pre-made UI components. This guide will show you how to integrate Clerk with ZenStack's [access control system](../../orm/access-control/). + +## Set up Clerk + +First, follow Clerk's [quick start guides](https://clerk.com/docs/quickstarts/overview) to set up your project if you haven't already. + +## Adjust your ZModel + +Since Clerk manages both user authentication and storage, you don't need to store users in your database anymore. However, you still need to provide a type that the `auth()` function can resolve to. Instead of using a regular model, we can declare a `type` instead: + +You can include any field you want in the `User` type, as long as you provide the same set of fields in the context object when calling `ZenStackClient`'s `$setAuth()` method. + +The following code shows an example blog post schema: + +```zmodel +type User { + id String @id + + @@auth +} + +model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + published Boolean @default(false) + authorId String // stores Clerk's user ID + + // author has full access + @@allow('all', auth() != null && auth().id == authorId) + + // logged-in users can view published posts + @@allow('read', auth() != null && published) +} +``` + +If you choose to [synchronize user data to your database](https://clerk.com/docs/users/sync-data-to-your-backend), you can define `User` as a regular `model` since it's then backed by a database table. + +## Create a user-bound ORM client + +When using ZenStack's built-in access control, you often use the `auth()` function in policy rules to reference the current user's identity. The evaluation of `auth()` at runtime requires you to call the `$setAuth()` method and pass in the validated user identity from Clerk. + +Please refer to clerk's documentation on how to fetch the current user on the server side for your specific framework. The following code shows an example for Next.js (app router): + +```ts +import { auth } from "@clerk/nextjs/server"; + +// ZenStack ORM client with access policy plugin installed +import { authDb } from './db'; + +async function getUserDb() { + // get the validated user identity from Clerk + const authObject = await auth(); + + // create a user-bound ORM client + return authDb.$setAuth( + authObject.userId ? { id: authObject.userId } : undefined); +} +``` diff --git a/versioned_docs/version-3.x/recipe/auth-integration/custom.md b/versioned_docs/version-3.x/recipe/auth-integration/custom.md new file mode 100644 index 00000000..282bd65f --- /dev/null +++ b/versioned_docs/version-3.x/recipe/auth-integration/custom.md @@ -0,0 +1,59 @@ +--- +description: Integrating with a custom authentication system. +sidebar_position: 100 +sidebar_label: Custom Authentication +--- + +# Custom Authentication Integration + +You may be using an authentication provider that's not mentioned in the guides. Or you may have a custom-implemented one. Integrating ZenStack with any authentication system is pretty straightforward. This guide will provide the general steps to follow. + +## Determine what's needed for access control + +The bridge that connects authentication and authorization is the `auth()` function that represents the authenticated current user. Based on your requirements, you should determine what fields are needed from it. The `auth()` object must at least contain an id field that uniquely identifies the current user. If you do RBAC, you'll very likely need an `auth().role` field available. Or even an `auth().permissions` field for fine-grained control. + +The `auth()` call needs to be resolved to a "model" or "type" in ZModel. If you store user data in your database, you may already have a "User" model that carries all the fields you need to access. + +```zmodel +model User { + id String @id + role String + permissions String[] + ... +} +``` + +ZenStack picks up the model named "User" automatically to resolve `auth()` unless another model or type is specifically appointed (by using the `@@auth` attribute). For example, if you're not storing user data locally, you can define a "type" to resolve `auth()`. This way, you can provide typing without being backed by a database table. + +```zmodel +type Auth { + id String @id + role String + permissions String[] + + @@auth +} +``` + +Just remember that any thing that you access from `auth().` must be resolved. + +## Fetch the current user along with the additional information + +At runtime in your backend code, you need to provide a value for the `auth()` call to ZenStack, so it can use it to evaluate access policies. How this is done is solely dependent on your authentication mechanism. Here are some examples: + +1. If you use JWT tokens, you can issue tokens with user id and other fields embedded, then validate and extract them from the request. +2. If you use a dedicated authentication service, you can call it to get the current user's information. + +You must ensure that whatever approach you use, the user information you get can be trusted and free of tampering. + +## Create a user-bound ORM client + +Finally, you can call the `$setAuth()` method on `ZenStackClient` to create an ORM instance that's bound to the current user. + +```ts +// ZenStack ORM client with access policy plugin installed +import { authDb } from './db'; + +const user = await getCurrentUser(); // your implementation +const db = authDb.$setAuth(user); +```