WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit ac93fd0

Browse files
author
Jeroen Peeters
committed
feat(clerk): add support for JWKS endpoint verification
1 parent b56d6ea commit ac93fd0

File tree

2 files changed

+69
-41
lines changed

2 files changed

+69
-41
lines changed

plugins/clerk/README.md

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Add the ClerkPlugin plugin to your Starbase configuration:
1111
```typescript
1212
import { ClerkPlugin } from './plugins/clerk'
1313
const clerkPlugin = new ClerkPlugin({
14+
dataSource,
1415
clerkInstanceId: 'ins_**********',
1516
clerkSigningSecret: 'whsec_**********',
1617
clerkSessionPublicKey: '-----BEGIN PUBLIC KEY***'
@@ -25,32 +26,44 @@ If you want to use the Clerk plugin to verify sessions, change the function `aut
2526

2627
```diff
2728
... existing code ...
29+
- if (!payload.sub) {
30+
+ if (!payload.sub || !await clerkPlugin.sessionExistsInDb(payload)) {
31+
throw new Error(
32+
'Invalid JWT payload, subject not found.'
33+
)
34+
}
35+
36+
context = payload
2837
} else {
29-
+ try {
30-
+ const authenticated = await clerkPlugin.authenticate(request, dataSource)
31-
+ if (!authenticated) {
32-
+ throw new Error('Unauthorized request')
33-
+ }
34-
+ } catch (error) {
35-
// If no JWT secret or JWKS endpoint is provided, then the request has no authorization.
36-
throw new Error('Unauthorized request')
37-
}
38+
+ const authenticated = await clerkPlugin.authenticate({
39+
+ cookie: request.headers.get("Cookie"),
40+
+ token,
41+
+ })
42+
// If no JWT secret or JWKS endpoint is provided, then the request has no authorization.
43+
- throw new Error('Unauthorized request')
44+
+ if (!authenticated) throw new Error('Unauthorized request')
3845
}
3946
... existing code ...
4047
```
4148

4249
## Configuration Options
4350

44-
| Option | Type | Default | Description |
45-
| ----------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |
46-
| `clerkSigningSecret` | string | `null` | Access your signing secret from (https://dashboard.clerk.com/last-active?path=webhooks) |
47-
| `clerkInstanceId` | string | `null` | (optional) Access your instance ID from (https://dashboard.clerk.com/last-active?path=settings) |
48-
| `verifySessions` | boolean | `true` | (optional) Verify sessions |
49-
| `clerkSessionPublicKey` | string | `null` | (optional) Access your public key from (https://dashboard.clerk.com/last-active?path=api-keys) |
50-
| `permittedOrigins` | string[] | `[]` | (optional) A list of allowed origins |
51+
| Option | Type | Default | Description |
52+
| ----------------------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- |
53+
| `dataSource` | DataSource | `null` | dataSource is needed to create tables and execute queries. |
54+
| `clerkSigningSecret` | string | `null` | Access your signing secret from (https://dashboard.clerk.com/last-active?path=webhooks) |
55+
| `clerkInstanceId` | string | `null` | (optional) Access your instance ID from (https://dashboard.clerk.com/last-active?path=settings) |
56+
| `clerkSessionPublicKey` | string | `null` | (optional) Access your public key from (https://dashboard.clerk.com/last-active?path=api-keys) if you want to verify using a public key |
57+
| `verifySessions` | boolean | `true` | (optional) Verify sessions, this creates a user_session table to store session data |
58+
| `permittedOrigins` | string[] | `[]` | (optional) A list of allowed origins |
5159

5260
## How To Use
5361

62+
### Available Methods
63+
64+
- `authenticate` - Authenticates a request using the Clerk session public key, returns true if authenticated, false in any other case.
65+
- `sessionExistsInDb` - Checks if a user session exists in the database, returns true if it does, false in any other case.
66+
5467
### Webhook Setup
5568

5669
For our Starbase instance to receive webhook events when user information changes, we need to add our plugin endpoint to Clerk.
@@ -66,3 +79,8 @@ For our Starbase instance to receive webhook events when user information change
6679
- Visit the API Keys page for your Clerk instance: https://dashboard.clerk.com/last-active?path=api-keys
6780
- Click the copy icon next to `JWKS Public Key`
6881
5. Copy the public key into the Clerk plugin
82+
6. Alternatively, you can use a JWKS endpoint instead of a public key.
83+
- Visit the API Keys page for your Clerk instance: https://dashboard.clerk.com/last-active?path=api-keys
84+
- Click the copy icon next to `JWKS URL`
85+
- Paste the URL under `AUTH_JWKS_ENDPOINT` in your `wrangler.toml`
86+
- Tweak the `authenticate` function in `src/index.ts` to check whether the session exists in the database, as shown in the [Usage](#usage) section.

plugins/clerk/index.ts

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class ClerkPlugin extends StarbasePlugin {
6868
clerkSessionPublicKey?: string
6969
verifySessions?: boolean
7070
permittedOrigins?: string[]
71+
dataSource: DataSource
7172
}) {
7273
super('starbasedb:clerk', {
7374
// The `requiresAuth` is set to false to allow for the webhooks sent by Clerk to be accessible
@@ -83,12 +84,11 @@ export class ClerkPlugin extends StarbasePlugin {
8384
this.clerkSessionPublicKey = opts.clerkSessionPublicKey
8485
this.verifySessions = opts.verifySessions ?? true
8586
this.permittedOrigins = opts.permittedOrigins ?? []
87+
this.dataSource = opts.dataSource
8688
}
8789

8890
override async register(app: StarbaseApp) {
89-
app.use(async (c, next) => {
90-
this.dataSource = c?.get('dataSource')
91-
91+
app.use(async (_, next) => {
9292
// Create user table if it doesn't exist
9393
await this.dataSource?.rpc.executeQuery({
9494
sql: SQL_QUERIES.CREATE_USER_TABLE,
@@ -190,28 +190,24 @@ export class ClerkPlugin extends StarbasePlugin {
190190
/**
191191
* Authenticates a request using the Clerk session public key.
192192
* heavily references https://clerk.com/docs/backend-requests/handling/manual-jwt
193-
* @param request The request to authenticate.
194-
* @param dataSource The data source to use for the authentication. Must be passed as a param as this can be called before the plugin is registered.
193+
* @param cookie The cookie to authenticate.
194+
* @param token The token to authenticate.
195195
* @returns {boolean} True if authenticated, false if not, undefined if the public key is not present.
196196
*/
197-
public async authenticate(request: Request, dataSource: DataSource): Promise<boolean | undefined> {
197+
public async authenticate({ cookie, token: tokenCrossOrigin }: { cookie?: string | null, token?: string }): Promise<boolean | undefined> {
198198
if (!this.verifySessions || !this.clerkSessionPublicKey) {
199-
throw new Error('Public key or session verification is not enabled.')
199+
console.error('Public key or session verification is not enabled.')
200+
return false
200201
}
201202

202203
const COOKIE_NAME = "__session"
203-
const cookie = parse(request.headers.get("Cookie") || "")
204-
const tokenSameOrigin = cookie[COOKIE_NAME]
205-
const tokenCrossOrigin = request.headers.get("Authorization")?.replace('Bearer ', '') ?? null
206-
207-
if (!tokenSameOrigin && !tokenCrossOrigin) {
208-
return false
209-
}
204+
const tokenSameOrigin = cookie ? parse(cookie)[COOKIE_NAME] : undefined
205+
if (!tokenSameOrigin && !tokenCrossOrigin) return false
210206

211207
try {
212208
const publicKey = await importSPKI(this.clerkSessionPublicKey, 'RS256')
213209
const token = tokenSameOrigin || tokenCrossOrigin
214-
const decoded = await jwtVerify(token!, publicKey)
210+
const decoded = await jwtVerify<{ sid: string; sub: string }>(token!, publicKey)
215211

216212
const currentTime = Math.floor(Date.now() / 1000)
217213
if (
@@ -229,23 +225,37 @@ export class ClerkPlugin extends StarbasePlugin {
229225
return false
230226
}
231227

232-
const sessionId = decoded.payload.sid
233-
const userId = decoded.payload.sub
234-
235-
const result: any = await dataSource?.rpc.executeQuery({
236-
sql: SQL_QUERIES.GET_SESSION,
237-
params: [sessionId, userId],
238-
})
239-
240-
if (!result?.length) {
228+
const sessionExists = await this.sessionExistsInDb(decoded.payload)
229+
if (!sessionExists) {
241230
console.error("Session not found")
242231
return false
243232
}
244233

245234
return true
246235
} catch (error) {
247236
console.error('Authentication error:', error)
248-
throw error
237+
return false
238+
}
239+
}
240+
241+
/**
242+
* Checks if a user session exists in the database.
243+
* @param sessionId The session ID to check.
244+
* @param userId The user ID to check.
245+
* @param dataSource The data source to use for the check.
246+
* @returns {boolean} True if the session exists, false if not.
247+
*/
248+
public async sessionExistsInDb(payload: { sub: string, sid: string }): Promise<boolean> {
249+
try {
250+
const result: any = await this.dataSource?.rpc.executeQuery({
251+
sql: SQL_QUERIES.GET_SESSION,
252+
params: [payload.sid, payload.sub],
253+
})
254+
255+
return result?.length > 0
256+
} catch (error) {
257+
console.error('db error while fetching session:', error)
258+
return false
249259
}
250260
}
251261
}

0 commit comments

Comments
 (0)