Skip to content

Commit 6277f84

Browse files
authored
Change pkg to auth0 api (#1)
* allow more versions of partyserver * 2.2.1 * minor change of impl * 2.3.0 * remove the static getCredentials - not needed * 3.0.0 * add experimental withownership mixin class * 3.1.0 * second attempt to fix types * 3.1.1 * add onAuthorizedConnect and onAuthorizedRequest * 3.2.0 * improve readme * 3.2.1 * fix on authorized request * 3.2.2 * feat: add aliases * 3.3.0 * fix issue with recreating server instance * 3.3.1 * convert package to auth0 sdk * convert package to auth0 sdk
1 parent c0f74ef commit 6277f84

7 files changed

Lines changed: 788 additions & 306 deletions

File tree

README.md

Lines changed: 181 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Agents OAuth2 JWT Bearer
22

3-
A PartyServer mixin for adding OAuth 2.0 JWT Bearer Token authentication to your PartyServer applications.
3+
A PartyServer mixin for adding OAuth 2.0 JWT Bearer Token authentication to your PartyServer applications, with Auth0 support.
44

55
It should work with:
66

@@ -9,16 +9,16 @@ It should work with:
99

1010
## Overview
1111

12-
This package provides a mixin that adds authentication functionality to a PartyServer server using [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://datatracker.ietf.org/doc/html/rfc9068). It allows you to secure your PartyServer applications by validating access tokens from requests and connections.
12+
This package provides a mixin that adds authentication functionality to a PartyServer server using [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://datatracker.ietf.org/doc/html/rfc9068). It allows you to secure your PartyServer applications by validating access tokens from requests and connections, with built-in support for Auth0.
1313

1414
## Installation
1515

1616
```bash
17-
npm install agents-oauth2-jwt-bearer
17+
npm install @auth0/auth0-cloudflare-agent-api
1818
# or
19-
yarn add agents-oauth2-jwt-bearer
19+
yarn add @auth0/auth0-cloudflare-agent-api
2020
# or
21-
pnpm add agents-oauth2-jwt-bearer
21+
pnpm add @auth0/auth0-cloudflare-agent-api
2222
```
2323

2424
## Usage
@@ -27,12 +27,12 @@ pnpm add agents-oauth2-jwt-bearer
2727

2828
```typescript
2929
import { Server } from "partyserver";
30-
import { WithAuth } from "agents-oauth2-jwt-bearer";
30+
import { WithAuth } from "@auth0/auth0-cloudflare-agent-api";
3131

3232
// Define your environment type
3333
type MyEnv = {
34-
OIDC_ISSUER_URL: string;
35-
OIDC_AUDIENCE: string;
34+
AUTH0_DOMAIN: string;
35+
AUTH0_AUDIENCE: string;
3636
// ... other environment variables
3737
};
3838

@@ -41,21 +41,21 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
4141
// Your server implementation
4242
}
4343

44-
// Pass verify options as parameters to the mixin function
44+
// Pass options as parameters to the mixin function
4545
class MyAuthenticatedServer extends WithAuth(Server, {
46-
// Optional: specify the audience and issuer, etc
47-
verify: verifyOptions,
48-
// Optional: allow unauthenticated requests and connections
46+
// Optional: make authentication optional
4947
authRequired: false,
48+
// Optional: provide a debug function
49+
debug: (message, ctx) => console.log(message, ctx),
5050
}) {
5151
// Your server implementation
5252
}
5353

5454
// Start the server
5555
const server = new MyAuthenticatedServer({
5656
env: {
57-
OIDC_ISSUER_URL: "https://your-identity-provider.com",
58-
OIDC_AUDIENCE: "your-api-audience",
57+
AUTH0_DOMAIN: "your-tenant.auth0.com",
58+
AUTH0_AUDIENCE: "your-api-audience",
5959
// ... other environment variables
6060
},
6161
});
@@ -73,7 +73,7 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
7373
onAuthenticatedRequest(req: Request) {
7474
// Get the JWT claims from the token
7575
const claims = this.getClaims();
76-
if (claims.sub !== expectedUserId) {
76+
if (claims?.sub !== expectedUserId) {
7777
return new Response("You are not welcome", { status: 401 });
7878
}
7979
}
@@ -86,7 +86,19 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
8686
const claims = this.getClaims();
8787

8888
// Now you can use the claims to identify the user
89-
console.log(`User ID: ${claims.sub}`);
89+
console.log(`User ID: ${claims?.sub}`);
90+
91+
// You can also require specific scopes for certain operations
92+
try {
93+
await this.requireAuth({ scopes: "read:data" });
94+
// The user has the required scope
95+
} catch (error) {
96+
if (error instanceof UnauthorizedError) {
97+
return error.toResponse();
98+
}
99+
// Handle other errors
100+
return new Response("Unknown error", { status: 500 });
101+
}
90102

91103
// Continue processing the request...
92104
return new Response("Hello authenticated user!");
@@ -109,7 +121,19 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
109121
const claims = this.getClaims();
110122

111123
// Use the claims in your connection handling logic
112-
console.log(`Connected user: ${claims.sub}`);
124+
console.log(`Connected user: ${claims?.sub}`);
125+
126+
// You can also require specific scopes for certain operations
127+
try {
128+
await this.requireAuth({ scopes: ["read:data", "write:data"] });
129+
// The user has both required scopes
130+
} catch (error) {
131+
if (error instanceof UnauthorizedError) {
132+
return error.terminateConnection(connection);
133+
}
134+
// Handle other errors
135+
throw error;
136+
}
113137
}
114138

115139
onMessage(connection: Connection, message: unknown) {
@@ -120,7 +144,7 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
120144
const claims = this.getClaims();
121145

122146
// Use the claims in your message handling logic
123-
console.log(`Message from user: ${claims.sub}`);
147+
console.log(`Message from user: ${claims?.sub}`);
124148

125149
// Process the message...
126150
}
@@ -132,8 +156,8 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
132156
1. When a client makes a request or connection:
133157

134158
- The mixin extracts the bearer token from the Authorization header or the `access_token` query parameter
135-
- It validates the token using the JWKS from your identity provider
136-
- It verifies the token's issuer and audience
159+
- It validates the token using Auth0's token verification API
160+
- It verifies the token's issuer and audience claims
137161

138162
2. If validation succeeds:
139163

@@ -148,26 +172,40 @@ class MyAuthenticatedServer extends WithAuth(Server<MyEnv>) {
148172

149173
The `WithAuth` mixin requires the following environment variables:
150174

151-
- `OIDC_ISSUER_URL`: The URL of the OpenID Connect issuer (your identity provider)
152-
- `OIDC_AUDIENCE`: The audience for the JWT, typically your API identifier
175+
- `AUTH0_DOMAIN`: Your Auth0 tenant domain (e.g., "your-tenant.auth0.com")
176+
- `AUTH0_AUDIENCE`: The audience for the JWT, typically your API identifier
177+
178+
You can also configure the mixin with options:
179+
180+
```typescript
181+
WithAuth(Server, {
182+
// Make authentication optional (default: true)
183+
authRequired: false,
184+
// Optional debug function
185+
debug: (message, context) => console.log(message, context),
186+
});
187+
```
153188

154189
## API Reference
155190

156-
### `WithAuth(BaseClass)`
191+
### `WithAuth(BaseClass, options?)`
157192

158193
A mixin factory function that adds authentication functionality to a PartyServer class.
159194

160195
**Parameters:**
161196

162197
- `BaseClass`: The base class to extend from. This should be a class that extends `Server`.
198+
- `options`: Optional configuration object:
199+
- `authRequired`: Boolean indicating whether authentication is required (default: true)
200+
- `debug`: Function for debugging (default: noop)
163201

164202
**Returns:**
165203

166204
- A new class that extends the base class with authentication capabilities.
167205

168206
### Methods
169207

170-
#### `getCredentials(): TokenSet | void`
208+
#### `getCredentials(): TokenSet | undefined`
171209

172210
Gets the token set associated with the current context.
173211

@@ -178,13 +216,32 @@ Gets the token set associated with the current context.
178216
- `id_token`: Optional ID token (from `x-id-token` header)
179217
- `refresh_token`: Optional refresh token (from `x-refresh-token` header)
180218

181-
#### `getClaims(reqOrConnection: Request | Connection): Record<string, unknown>`
219+
#### `getClaims(): Token | undefined`
182220

183221
Gets the decoded JWT claims from the access token.
184222

185223
**Returns:**
186224

187-
- An object containing the JWT claims
225+
- An object containing the JWT claims or undefined if no token is available
226+
227+
#### `requireAuth(options?: { scopes?: string | string[] }): Promise<TokenSet>`
228+
229+
Requires authentication with optional scope checking.
230+
231+
**Parameters:**
232+
233+
- `options`: Optional configuration object:
234+
- `scopes`: String or array of strings representing required scopes
235+
236+
**Returns:**
237+
238+
- A promise that resolves to the token set if authentication is successful
239+
240+
**Throws:**
241+
242+
- `UnauthorizedError`: If no valid token is present
243+
- `InvalidTokenError`: If the token is invalid
244+
- `InsufficientScopeError`: If the token doesn't have the required scopes
188245

189246
## Token Format
190247

@@ -193,8 +250,106 @@ The mixin accepts tokens in the following formats:
193250
1. Authorization header: `Authorization: Bearer <token>`
194251
2. Query parameter: `?access_token=<token>`
195252

253+
## Advanced Usage: WithOwnership Mixin
254+
255+
### Overview
256+
257+
The `WithOwnership` mixin adds ownership capabilities to a PartyServer that already has authentication provided by the `WithAuth` mixin. This is particularly useful for scenarios where you need to restrict access to resources based on ownership, such as private chats or user-specific data.
258+
259+
### Key Features
260+
261+
- Owner-based access control for connections and requests
262+
- Integration with Durable Objects for persistent ownership data
263+
- Automatic rejection of non-owner access attempts
264+
265+
### Usage Example
266+
267+
```typescript
268+
// Then add ownership with WithOwnership
269+
class MyServer extends WithOwnership(WithAuth(Server<MyEnv>), {
270+
// Optional: provide a debug function
271+
debug: (message, ctx) => console.log(message, ctx),
272+
}) {
273+
// Your server implementation
274+
275+
// Optionally override authorization methods
276+
async onAuthorizedConnect(connection, ctx) {
277+
console.log("Owner connected:", this.getClaims()?.sub);
278+
// Handle authorized connection
279+
}
280+
281+
async onAuthorizedRequest(req) {
282+
console.log("Owner made a request:", this.getClaims()?.sub);
283+
// Handle authorized request
284+
}
285+
}
286+
```
287+
288+
### Ownership Methods
289+
290+
#### `setOwner(owner: string, overwrite: boolean = false): Promise<void>`
291+
292+
Sets the owner of the object. By default, it will throw an error if the owner is already set to a different user unless `overwrite` is set to `true`.
293+
294+
**Parameters:**
295+
296+
- `owner`: The user ID (sub from JWT claims) to set as the owner
297+
- `overwrite`: Optional boolean to allow overwriting an existing owner
298+
299+
**Example:**
300+
301+
```typescript
302+
// When initializing a new chat or resource
303+
async onCreate() {
304+
const claims = this.getClaims();
305+
if (claims?.sub) {
306+
await this.setOwner(claims.sub);
307+
}
308+
}
309+
```
310+
311+
#### `getOwner(): Promise<string | undefined>`
312+
313+
Gets the current owner of the object.
314+
315+
**Returns:**
316+
317+
- The user ID (sub) of the owner, or undefined if no owner is set
318+
319+
**Example:**
320+
321+
```typescript
322+
async checkOwnership() {
323+
const owner = await this.getOwner();
324+
console.log(`This resource is owned by: ${owner}`);
325+
}
326+
```
327+
328+
### Authorization Flow
329+
330+
1. When a client makes a request or connection:
331+
332+
- First, the authentication checks are performed by the `WithAuth` mixin
333+
- Then, the ownership check verifies if the authenticated user is the owner
334+
335+
2. If the ownership check succeeds:
336+
337+
- The `onAuthorizedConnect` or `onAuthorizedRequest` method is called
338+
- The connection or request is allowed to proceed
339+
340+
3. If the ownership check fails:
341+
- For WebSocket connections: Connection is closed with code 1008 and message "This chat is not yours."
342+
- For HTTP requests: A 403 Forbidden response is returned with message "This chat is not yours."
343+
344+
### DurableObject Integration
345+
346+
The `WithOwnership` mixin is designed to work with Cloudflare DurableObjects for storing ownership data. The mixin uses the DurableObject's storage API to persist ownership information.
347+
348+
**Note:** If you're not using DurableObjects, you'll need to override the `setOwner` and `getOwner` methods to implement your own storage mechanism.
349+
196350
## References
197351

352+
- This project uses the Auth0 API Client to verify access tokens: [@auth0/auth0-api-js](https://github.com/auth0/auth0-api-js)
198353
- This project is similar to other Auth0 middlewares like [node-oauth2-jwt-bearer](https://github.com/auth0/node-oauth2-jwt-bearer).
199354
- [Authentication on PartyKit](https://docs.partykit.io/guides/authentication/).
200355

package.json

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
2-
"name": "@auth0/agents-oauth2-jwt-bearer",
3-
"version": "2.2.0",
4-
"private": true,
5-
"description": "A PartyServer mixin for adding OAuth 2.0 JWT Bearer Token authentication to your PartyServer applications.",
2+
"name": "@auth0/auth0-cloudflare-agent-api",
3+
"version": "3.3.1",
4+
"description": "A PartyServer mixin for adding Auth0 API authentication to your PartyServer applications.",
65
"author": {
76
"name": "Auth0 Inc.",
87
"url": "https://auth0.com"
@@ -42,10 +41,10 @@
4241
"typescript-eslint": "^8.30.1",
4342
"vitest": "^3.1.1"
4443
},
45-
"dependencies": {
46-
"jose": "^6.0.10"
47-
},
4844
"peerDependencies": {
49-
"partyserver": "^0.0.67 || ^0.0.68"
45+
"partyserver": ">=0.0.67 <0.0.72"
46+
},
47+
"dependencies": {
48+
"@auth0/auth0-api-js": "^1.0.1"
5049
}
5150
}

src/bearer/errors.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Errors per https://tools.ietf.org/html/rfc6750#section-3.1
33
*/
44

5+
import { Connection } from "partyserver";
6+
57
/**
68
* If the request lacks any authentication information,
79
* the resource server SHOULD NOT include an error code or
@@ -16,6 +18,27 @@ export class UnauthorizedError extends Error {
1618
super(message);
1719
this.name = this.constructor.name;
1820
}
21+
22+
/**
23+
* Respond with a 401 Unauthorized error.
24+
* This is used for HTTP requests.
25+
* @returns
26+
*/
27+
toResponse() {
28+
return new Response(null, {
29+
status: this.statusCode,
30+
headers: this.headers,
31+
});
32+
}
33+
34+
/**
35+
* Terminate a websocket connection
36+
* due an authorization error.
37+
* @param connection -
38+
*/
39+
terminateConnection(connection: Connection) {
40+
connection.close(1008, this.message);
41+
}
1942
}
2043

2144
/**

0 commit comments

Comments
 (0)