Skip to content

Commit 3af1837

Browse files
Improve Readme
1 parent f88f21b commit 3af1837

12 files changed

+211
-60
lines changed

.dev.vars.example

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# passwordless, authenticated role
2-
DATABASE_URL=
2+
DATABASE_AUTHENTICATED_URL=
33
# neondb_owner role
4-
OWNER_DATABASE_URL=
4+
DATABASE_URL=

README.md

+175-42
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,177 @@
11
<img width="250px" src="https://neon.tech/brand/neon-logo-dark-color.svg" />
22

3-
# Neon Authorize Demo with Custom JWTs
4-
5-
This is a project that showcases how to use Neon Authorize with custom JWTs. Instead of relying on JWTs that are generated by an authentication provider (Clerk, Auth0, etc.), this project uses a custom JWT that is signed by the server.
6-
7-
This is a [HONC](https://honc.dev/) API which exposes a few endpoints:
8-
* `/api/token` — returns a custom JWT that is signed by the server
9-
* `/.well-known/jwks.json` — returns the public key that can be used to verify the signature of the custom JWT (this is used by Neon Authorize to verify the signature of the custom JWT)
10-
* `/api/users` — returns a list of users
11-
* `/api/tenants` — returns a list of tenants
12-
13-
The schema is generated by Drizzle and can be found in `src/db/schema.ts`. The generated SQL is in the `drizzle/` directory. There's also a `seed.sql` file that can be used to seed the database with some data, that is specific to this demo.
14-
15-
## Steps to run the project
16-
1. Generate the keys that will be used to sign the JWT. You can run `bun generate-keys.ts` and you will get a `publicKey.jwk.json` and a `privateKey.jwk.json` file.
17-
2. Create a Neon project
18-
3. Set up a `wrangler.toml` file with the following configuration:
19-
20-
```toml
21-
name = "my-honc-service"
22-
compatibility_date = "2024-07-25"
23-
compatibility_flags = [ "nodejs_compat" ]
24-
25-
[vars]
26-
# neondb_owner role
27-
OWNER_DATABASE_URL = ""
28-
# authenticated, passwordless role (you can keep this empty for now)
29-
DATABASE_URL = ""
30-
# contents of publicKey.jwk.json
31-
PUBLIC_KEY=''
32-
# contents of privateKey.jwk.json
33-
PRIVATE_KEY=''
34-
```
35-
36-
3. Deploy this demo with `bun run deploy`.
37-
4. Go to `https://my-honc-service.<your-name>.workers.dev/.well-known/jwks.json` to verify the public key is being served appropriately.
38-
5. Go to the Authorize page in the Neon console and add an auth provider (type should be "Other"), and set the JWKS URL to the URL from the previous step.
39-
6. Follow the steps in the UI to setup the roles for Neon Authorize. You should ignore the schema related steps if you're following this guide
40-
7. Apply migrations with `bun run db:migrate` (you'll have to populate `.dev.vars` with the database URLs from the Neon console). Notice that there's 2 different database URLs that are expected in the `.dev.vars` file. The first one is for the `neondb_owner` role, and the second one is for the `authenticated, passwordless` role.
41-
8. Seed the database with `bun run db:seed`.
42-
9. Grab the `authenticated` role's database URL from the Neon console and set it in the `.dev.vars` file, as well as in the `wrangler.toml` file.
43-
10. Deploy this demo again with `bun run deploy`.
44-
11. Head to `https://my-honc-service.<your-name>.workers.dev/api/users` and `https://my-honc-service.<your-name>.workers.dev/api/tenants` to verify that the API is working as intended.
3+
# Neon Authorize Demo: Securing Your Application with Custom JWTs and Row-Level Security
4+
5+
This project demonstrates how to leverage **Neon Authorize** using Custom JWT's. Instead of relying on JWTs generated by third-party authentication providers (like Clerk or Auth0), this demo showcases how to create and use JWTs signed directly by your server.
6+
7+
### **Why is this important?**
8+
9+
Neon Authorize empowers you to move authorization logic closer to your data in PostgreSQL. By using custom JWTs, you maintain complete control over the token generation process while still benefiting from Neon Authorize's seamless integration with RLS. This approach is valuable when you have specific requirements for your JWT structure or want to avoid dependencies on external auth providers for certain internal workflows.
10+
11+
This demo is built as a [HONC](https://honc.dev/) API and exposes the following endpoints to illustrate these concepts:
12+
13+
- `/token` — Generates and returns a custom JWT signed by the server. This token contains claims that will be used by PostgreSQL to enforce RLS. The token includes a `tenant_id` claim that determines the data access rights in this example.
14+
- `/.well-known/jwks.json` — Serves the JSON Web Key Set (JWKS) containing the public key. Neon Authorize uses this to verify the signature of the custom JWTs you provide.
15+
- `/api/users` — Retrieves a list of users. Access to individual user records is controlled by RLS based on the `sub` claim in the JWT.
16+
- `/api/tenants` — Retrieves a list of tenants. Access to tenant records is also controlled by RLS based on the `tenant_id` claim in the JWT.
17+
18+
The database schema is defined using [Drizzle ORM](https://orm.drizzle.team/) and can be found in `src/db/schema.ts`. The generated SQL migrations are located in the `drizzle/` directory. A `seed.sql` file is included to populate the database with initial demo data.
19+
20+
## Prerequisites
21+
22+
Before you begin, ensure you have the following installed:
23+
24+
- [Bun](https://bun.sh/) (or Node.js with npm/yarn/pnpm)
25+
- [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) (for deploying to Cloudflare Workers)
26+
- A [Neon](https://neon.tech/) account and project
27+
28+
## Getting Started
29+
30+
Follow these steps to run the demo:
31+
32+
1. Install the dependencies:
33+
34+
```bash
35+
bun install
36+
```
37+
38+
2. **Generate Key Pair:** Generate the public and private keys used for signing the JWTs. Run the following command:
39+
40+
```bash
41+
bun generate-keys.ts
42+
```
43+
44+
This will create `publicKey.jwk.json` and `privateKey.jwk.json` files containing the respective keys in JWK format.
45+
46+
3. **Set Up Neon Project:**
47+
48+
- Create a new project in your Neon account.
49+
- Copy the connection string from Neon Console.
50+
![Neon Connection String](./images/neon-dashboard.png)
51+
52+
4. **Configure `wrangler.toml`:** Create your `wrangler.toml` file with the following configuration. **Replace the placeholder values** with your actual Neon connection strings and the content of your generated key files:
53+
54+
```toml
55+
name = "my-honc-service"
56+
compatibility_date = "2024-07-25"
57+
compatibility_flags = [ "nodejs_compat" ]
58+
59+
[vars]
60+
# neondb_owner role - used for migrations and seeding, which you copied from Neon Console
61+
DATABASE_URL = "postgres://<user>:<password>@<host>:<port>/<database>?sslmode=require"
62+
# authenticated, passwordless role (you can keep this empty for now)
63+
DATABASE_AUTHENTICATED_URL = ""
64+
# Contents of publicKey.jwk.json
65+
PUBLIC_KEY='{"kty":"RSA", ... }'
66+
# Contents of privateKey.jwk.json
67+
PRIVATE_KEY='{"kty":"RSA", ... }'
68+
```
69+
70+
**Important:** Make sure to copy the _content_ of the JWK files into the `PUBLIC_KEY` and `PRIVATE_KEY` variables as strings.
71+
72+
5. **Deploy to Cloudflare Workers:** Deploy the demo application:
73+
74+
```bash
75+
bun run deploy
76+
```
77+
78+
6. **Verify Public Key Endpoint:** After deployment, verify that the public key is being served correctly by visiting the JWKS endpoint in your browser. Replace `<your-name>` with your Cloudflare Workers subdomain:
79+
80+
```
81+
https://my-honc-service.<your-name>.workers.dev/.well-known/jwks.json
82+
```
83+
84+
![Cloudflare Workers JWKS Endpoint](./images/jwks-endpoint.png)
85+
86+
You should see the contents of your `publicKey.jwk.json` file.
87+
88+
7. **Configure Neon Authorize:**
89+
90+
- Go to the **Authorize** page in your Neon console.
91+
- Add a new authentication provider.
92+
- Set the **JWKS URL** to the URL you verified in the previous step (e.g., `https://my-honc-service.<your-name>.workers.dev/.well-known/jwks.json`).
93+
![Neon Authorize Configuration](./images/neon-authorize-add-jwks.png)
94+
- Follow the steps in the UI to setup the roles for Neon Authorize. You should ignore the schema related steps if you're following this guide
95+
- Note down the connection strings for both the **`neondb_owner` role** and the **`authenticated, passwordless` role**. You'll need both. The `neondb_owner` role has full privileges and is used for migrations, while the `authenticated` role will be used by the application and will have its access restricted by RLS.
96+
![Neon Authorize env vars](./images/neon-authorize-env-values.png)
97+
98+
8. **Apply Database Migrations:**
99+
100+
- Create a `.dev.vars` file in the project root and populate it with your Neon database URLs:
101+
102+
```bash
103+
cp .dev.vars.example .dev.vars
104+
```
105+
106+
Update the `.dev.vars` file with the connection strings you noted down from Neon Authorize:
107+
108+
```env
109+
DATABASE_AUTHENTICATED_URL="postgres://<user>.<role>:<password>@<host>:<port>/<database>?sslmode=require" # authenticated role
110+
DATABASE_URL="postgres://<user>:<password>@<host>:<port>/<database>?sslmode=require" # neondb_owner role
111+
```
112+
113+
- Apply the database migrations using the `neondb_owner` role:
114+
115+
```bash
116+
bun run db:migrate
117+
```
118+
119+
9. **Seed the Database:** Seed the database with initial data using the `neondb_owner` role:
120+
121+
```bash
122+
bun run db:seed
123+
```
124+
125+
10. **Update `wrangler.toml`** Ensure that the `DATABASE_AUTHENTICATED_URL` in your `wrangler.toml` file is set to the connection string for the **`authenticated` role**.
126+
127+
11. **Redeploy:** Deploy the application again to ensure the correct `DATABASE_AUTHENTICATED_URL` for the authenticated role is used:
128+
129+
```bash
130+
bun run deploy
131+
```
132+
133+
12. **Verify API Endpoints:** Access the API endpoints in your browser to verify that the RLS is working as expected.
134+
135+
- **Users Endpoint:** Visiting this endpoint will use a custom JWT with a specific `tenant_id`. You should only see users associated with that tenant.
136+
137+
```
138+
https://my-honc-service.<your-name>.workers.dev/api/users
139+
```
140+
141+
- **Tenants Endpoint:** Similarly, this endpoint's access will be filtered based on the `tenant_id` in the JWT.
142+
143+
```
144+
https://my-honc-service.<your-name>.workers.dev/api/tenants
145+
```
146+
147+
## Understanding the Code
148+
149+
- **`generate-keys.ts`:** This script generates the RSA key pair used for signing and verifying JWTs.
150+
- **`src/index.ts`:** This is the main application logic. It defines the API endpoints, generates custom JWTs, and interacts with the Neon database using Drizzle ORM. Notice how the `authToken` is included when creating the Drizzle client, allowing Neon Authorize to enforce RLS based on the JWT claims.
151+
- **`src/db/schema.ts`:** Defines the database schema using Drizzle ORM, including the Row-Level Security policies that restrict access based on the `auth.session()->>'tenant_id'` and `auth.user_id()` values derived from the JWT.
152+
- **`seed.sql` & `seed.ts`:** These files contain the initial data and the script to seed the database. Pay attention to the `GRANT` statement in `seed.sql`, which grants permissions to the `authenticated` role, the role under which RLS policies are enforced.
153+
154+
## Key Concepts Demonstrated
155+
156+
- **Custom JWT Generation:** The demo showcases how to create and sign JWTs directly within your application, giving you full control over the claims included.
157+
- **Neon Authorize Integration:** The project demonstrates how to configure Neon Authorize to trust your custom JWTs by providing the JWKS URL.
158+
- **Row-Level Security (RLS):** The database schema includes RLS policies that use the `auth.session()` and `auth.user_id()` functions (provided by the `pg_session_jwt` extension, which Neon Authorize leverages) to enforce access control based on the claims in the JWT.
159+
- **Multi-Tenancy:** The example demonstrates a basic multi-tenant scenario where users and data are associated with specific tenants, and access is restricted accordingly.
160+
161+
## Next Steps and Further Exploration
162+
163+
- **Explore Different JWT Claims:** Experiment with adding more custom claims to your JWTs and create corresponding RLS policies to enforce different access control rules. Currently the JWT uses hardcoded `tenant_id` and `sub` claims, but you will likely want to use dynamic values based on the authenticated user.
164+
- **Implement User Authentication:** Integrate a proper authentication mechanism to issue these custom JWTs to authenticated users.
165+
166+
## Learn More
167+
168+
- [Neon Authorize Tutorial](https://neon.tech/docs/guides/neon-authorize-tutorial)
169+
- [Simplify RLS with Drizzle](https://neon.tech/docs/guides/neon-authorize-drizzle)
170+
171+
## Authors
172+
173+
- [David Gomes](https://github.com/davidgomes)
174+
175+
## Contributing
176+
177+
Contributions are welcome! Please feel free to submit a Pull Request.

bun.lockb

-2.93 KB
Binary file not shown.

drizzle.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ export default defineConfig({
88
out: './drizzle',
99
dialect: 'postgresql',
1010
dbCredentials: {
11-
url: process.env.OWNER_DATABASE_URL!,
11+
url: process.env.DATABASE_URL!,
1212
}
1313
})

generate-keys.ts

-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
// generate-keys.ts
2-
31
import { generateKeyPair, exportJWK } from 'jose';
42
import fs from 'fs';
53

64
async function generateAndExportKeys() {
7-
// Set extractable to true
85
const { publicKey, privateKey } = await generateKeyPair('RS256', { extractable: true });
96

107
const privateJwk = await exportJWK(privateKey);

images/jwks-endpoint.png

78.2 KB
Loading

images/neon-authorize-add-jwks.png

136 KB
Loading

images/neon-authorize-env-values.png

82.4 KB
Loading

images/neon-dashboard.png

101 KB
Loading

package.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
"db:seed": "tsx seed.ts"
99
},
1010
"dependencies": {
11-
"@neondatabase/serverless": "^0.10.1",
11+
"@neondatabase/serverless": "^0.10.4",
1212
"dotenv": "^16.4.5",
13-
"drizzle-orm": "^0.34.1-a5ec472",
13+
"drizzle-orm": "^0.38.3",
1414
"hono": "^4.5.0",
15-
"jose": "^5.9.4"
15+
"jose": "^5.9.4",
16+
"uuid": "^11.0.4"
1617
},
1718
"devDependencies": {
18-
"@cloudflare/workers-types": "^4.20240403.0",
19-
"drizzle-kit": "^0.25.0-a5ec472",
19+
"@cloudflare/workers-types": "^4.20250109.0",
20+
"drizzle-kit": "^0.30.1",
2021
"tsx": "^4.11.0",
2122
"wrangler": "^3.47.0"
2223
}

seed.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11

22
import { drizzle } from "drizzle-orm/neon-http";
33
import { neon } from "@neondatabase/serverless";
4-
import { users } from "./src/db/schema";
4+
import { tenants, users } from "./src/db/schema";
55
import { config } from "dotenv";
6+
import { v4 as uuidv4 } from 'uuid';
67

78
config({ path: ".dev.vars" });
89

@@ -11,15 +12,33 @@ const sql = neon(process.env.DATABASE_URL!);
1112
const db = drizzle(sql);
1213

1314
async function seed() {
15+
// Create a tenant
16+
const tenantId = "f330c503-5e9c-46a7-8393-2995aeb03675";
17+
await db.insert(tenants).values([
18+
{
19+
id: tenantId
20+
}
21+
]);
22+
23+
// Create some users
1424
await db.insert(users).values([
1525
{
26+
tenantId: tenantId,
27+
id: "2e7e25e8-5445-40bd-8f89-dc19bba64faa", // Using same id as the subject in index.ts. This would be shown on hitting /api/users and remaining users are not shown
1628
name: "Laszlo Cravensworth",
29+
1730
},
1831
{
32+
tenantId: tenantId,
33+
id: uuidv4(),
1934
name: "Nadja Antipaxos",
35+
2036
},
2137
{
38+
tenantId: tenantId,
39+
id: uuidv4(),
2240
name: "Colin Robinson",
41+
2342
},
2443
]);
2544
}

src/index.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ import { tenants, users } from "./db/schema";
55
import { SignJWT, JWK } from "jose";
66

77
type Bindings = {
8-
DATABASE_URL: string;
8+
DATABASE_AUTHENTICATED_URL: string;
99
PRIVATE_KEY: string;
1010
PUBLIC_KEY: string;
1111
};
1212

1313
const app = new Hono<{ Bindings: Bindings }>();
1414

1515
async function createJWT(privateKey: JWK) {
16+
// This example demonstrates JWT token creation and signing. When integrating with your application, ensure that the tenant_id and user_id are dynamically adjusted based on the user's session.
1617
const jwt = await new SignJWT({
17-
tenant_id: "f330c503-5e9c-46a7-8393-2995aeb03675",
18+
tenant_id: "f330c503-5e9c-46a7-8393-2995aeb03675", // As an example, the tenant id is being hardcoded here
1819
})
1920
.setProtectedHeader({ alg: "RS256", kid: "my-key-id" })
20-
.setSubject("2e7e25e8-5445-40bd-8f89-dc19bba64faa")
21+
.setSubject("2e7e25e8-5445-40bd-8f89-dc19bba64faa") // As an example, the user id is being hardcoded here
2122
.setExpirationTime("1h")
2223
.setIssuedAt()
2324
.sign(privateKey);
@@ -49,7 +50,7 @@ app.get("/api/users", async (c) => {
4950
const privateKeyJwk: JWK = JSON.parse(c.env.PRIVATE_KEY);
5051
const authToken = await createJWT(privateKeyJwk);
5152

52-
const db = drizzle(neon(c.env.DATABASE_URL, {
53+
const db = drizzle(neon(c.env.DATABASE_AUTHENTICATED_URL, {
5354
authToken,
5455
}));
5556

@@ -62,12 +63,12 @@ app.get("/api/tenants", async (c) => {
6263
const privateKeyJwk: JWK = JSON.parse(c.env.PRIVATE_KEY);
6364
const authToken = await createJWT(privateKeyJwk);
6465

65-
const db = drizzle(neon(c.env.DATABASE_URL, {
66+
const db = drizzle(neon(c.env.DATABASE_AUTHENTICATED_URL, {
6667
authToken,
6768
}));
6869

6970
return c.json({
70-
users: await db.select().from(tenants),
71+
tenants: await db.select().from(tenants),
7172
});
7273
});
7374

0 commit comments

Comments
 (0)