Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions versioned_docs/version-3.x/recipe/auth-integration/better-auth.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
sidebar_position: 1
sidebar_label: Better-Auth
---

import PackageInstall from '../../_components/PackageInstall';
Expand All @@ -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, {
Expand All @@ -45,6 +48,8 @@ Then, run the "generate" command to generate the schema:

<PackageExec command="@better-auth/cli generate" />

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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()`:
Expand Down
68 changes: 68 additions & 0 deletions versioned_docs/version-3.x/recipe/auth-integration/clerk.md
Original file line number Diff line number Diff line change
@@ -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);
}
```
59 changes: 59 additions & 0 deletions versioned_docs/version-3.x/recipe/auth-integration/custom.md
Original file line number Diff line number Diff line change
@@ -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);
```