@codefox-inc/oauth-provider 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +572 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/auth-config.d.ts +85 -0
  8. package/dist/client/auth-config.d.ts.map +1 -0
  9. package/dist/client/auth-config.js +81 -0
  10. package/dist/client/auth-config.js.map +1 -0
  11. package/dist/client/auth-helper.d.ts +81 -0
  12. package/dist/client/auth-helper.d.ts.map +1 -0
  13. package/dist/client/auth-helper.js +97 -0
  14. package/dist/client/auth-helper.js.map +1 -0
  15. package/dist/client/index.d.ts +189 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +230 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/routes.d.ts +94 -0
  20. package/dist/client/routes.d.ts.map +1 -0
  21. package/dist/client/routes.js +113 -0
  22. package/dist/client/routes.js.map +1 -0
  23. package/dist/component/_generated/api.d.ts +44 -0
  24. package/dist/component/_generated/api.d.ts.map +1 -0
  25. package/dist/component/_generated/api.js +31 -0
  26. package/dist/component/_generated/api.js.map +1 -0
  27. package/dist/component/_generated/component.d.ts +123 -0
  28. package/dist/component/_generated/component.d.ts.map +1 -0
  29. package/dist/component/_generated/component.js +11 -0
  30. package/dist/component/_generated/component.js.map +1 -0
  31. package/dist/component/_generated/dataModel.d.ts +46 -0
  32. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  33. package/dist/component/_generated/dataModel.js +11 -0
  34. package/dist/component/_generated/dataModel.js.map +1 -0
  35. package/dist/component/_generated/server.d.ts +121 -0
  36. package/dist/component/_generated/server.d.ts.map +1 -0
  37. package/dist/component/_generated/server.js +78 -0
  38. package/dist/component/_generated/server.js.map +1 -0
  39. package/dist/component/clientManagement.d.ts +39 -0
  40. package/dist/component/clientManagement.d.ts.map +1 -0
  41. package/dist/component/clientManagement.js +169 -0
  42. package/dist/component/clientManagement.js.map +1 -0
  43. package/dist/component/constants.d.ts +31 -0
  44. package/dist/component/constants.d.ts.map +1 -0
  45. package/dist/component/constants.js +36 -0
  46. package/dist/component/constants.js.map +1 -0
  47. package/dist/component/convex.config.d.ts +3 -0
  48. package/dist/component/convex.config.d.ts.map +1 -0
  49. package/dist/component/convex.config.js +3 -0
  50. package/dist/component/convex.config.js.map +1 -0
  51. package/dist/component/handlers.d.ts +143 -0
  52. package/dist/component/handlers.d.ts.map +1 -0
  53. package/dist/component/handlers.js +624 -0
  54. package/dist/component/handlers.js.map +1 -0
  55. package/dist/component/mutations.d.ts +111 -0
  56. package/dist/component/mutations.d.ts.map +1 -0
  57. package/dist/component/mutations.js +459 -0
  58. package/dist/component/mutations.js.map +1 -0
  59. package/dist/component/queries.d.ts +127 -0
  60. package/dist/component/queries.d.ts.map +1 -0
  61. package/dist/component/queries.js +145 -0
  62. package/dist/component/queries.js.map +1 -0
  63. package/dist/component/schema.d.ts +116 -0
  64. package/dist/component/schema.d.ts.map +1 -0
  65. package/dist/component/schema.js +77 -0
  66. package/dist/component/schema.js.map +1 -0
  67. package/dist/component/token_security.d.ts +53 -0
  68. package/dist/component/token_security.d.ts.map +1 -0
  69. package/dist/component/token_security.js +91 -0
  70. package/dist/component/token_security.js.map +1 -0
  71. package/dist/lib/convex-types.d.ts +21 -0
  72. package/dist/lib/convex-types.d.ts.map +1 -0
  73. package/dist/lib/convex-types.js +2 -0
  74. package/dist/lib/convex-types.js.map +1 -0
  75. package/dist/lib/oauth.d.ts +123 -0
  76. package/dist/lib/oauth.d.ts.map +1 -0
  77. package/dist/lib/oauth.js +295 -0
  78. package/dist/lib/oauth.js.map +1 -0
  79. package/dist/react/index.d.ts +2 -0
  80. package/dist/react/index.d.ts.map +1 -0
  81. package/dist/react/index.js +6 -0
  82. package/dist/react/index.js.map +1 -0
  83. package/package.json +121 -0
  84. package/src/client/__tests__/auth-config.test.ts +244 -0
  85. package/src/client/__tests__/auth-helper.test.ts +273 -0
  86. package/src/client/__tests__/oauth-provider.test.ts +418 -0
  87. package/src/client/__tests__/routes.test.ts +428 -0
  88. package/src/client/_generated/_ignore.ts +1 -0
  89. package/src/client/auth-config.ts +157 -0
  90. package/src/client/auth-helper.ts +201 -0
  91. package/src/client/index.ts +326 -0
  92. package/src/client/routes.ts +251 -0
  93. package/src/component/__tests__/oauth.test.ts +3310 -0
  94. package/src/component/__tests__/rfc-compliance.test.ts +788 -0
  95. package/src/component/__tests__/token-security.test.ts +133 -0
  96. package/src/component/_generated/api.ts +60 -0
  97. package/src/component/_generated/component.ts +201 -0
  98. package/src/component/_generated/dataModel.ts +60 -0
  99. package/src/component/_generated/server.ts +156 -0
  100. package/src/component/clientManagement.ts +189 -0
  101. package/src/component/constants.ts +40 -0
  102. package/src/component/convex.config.ts +3 -0
  103. package/src/component/handlers.ts +964 -0
  104. package/src/component/mutations.ts +531 -0
  105. package/src/component/queries.ts +165 -0
  106. package/src/component/schema.ts +92 -0
  107. package/src/component/token_security.ts +102 -0
  108. package/src/lib/__tests__/oauth-helpers.test.ts +143 -0
  109. package/src/lib/__tests__/oauth-jwt.test.ts +405 -0
  110. package/src/lib/convex-types.ts +37 -0
  111. package/src/lib/oauth.ts +412 -0
  112. package/src/react/index.ts +7 -0
  113. package/src/test.ts +21 -0
package/README.md ADDED
@@ -0,0 +1,572 @@
1
+ # @codefox-inc/oauth-provider
2
+
3
+ OAuth 2.1 / OpenID Connect Provider implemented as a Convex component.
4
+
5
+ > **⚠️ Beta Software**
6
+ > Built for [Convex Auth](https://labs.convex.dev/auth) which is currently in **Beta**.
7
+ > Expect breaking changes. Production use at your own risk.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm add @codefox-inc/oauth-provider
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - **OAuth 2.1 compliant** authorization and token endpoints
18
+ - **OpenID Connect Discovery** for automatic client configuration
19
+ - **PKCE required** for all authorization code flows (S256 only)
20
+ - **Secure token storage** using SHA-256 hashing for tokens and authorization codes
21
+ - **JWT access tokens** with RS256 signing
22
+ - **Refresh token rotation** for enhanced security
23
+ - **Dynamic client registration** (opt-in)
24
+ - **Authorization management** for user consent tracking
25
+ - **JWKS endpoint** for token verification
26
+
27
+ ## OAuth 2.1 Compliance
28
+
29
+ This implementation follows [OAuth 2.1 (draft-ietf-oauth-v2-1-14)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-14) specification:
30
+
31
+ ### Supported Grant Types
32
+ - ✅ **Authorization Code with PKCE** (public and confidential clients)
33
+ - ✅ **Refresh Token** (with token rotation)
34
+
35
+ ### Unsupported Features (OAuth 2.0 Legacy)
36
+ - ❌ **Implicit Grant** (removed in OAuth 2.1 for security reasons)
37
+ - ❌ **Resource Owner Password Credentials Grant** (removed in OAuth 2.1)
38
+ - ❌ **PKCE Plain Method** (only S256 is supported per OAuth 2.1 best practices)
39
+
40
+ ### Key Security Requirements
41
+ - **PKCE Enforcement**: All authorization code flows require PKCE with S256 method
42
+ - **Redirect URI Validation**: Exact string matching (with localhost variable port exception per RFC 8252)
43
+ - **Authorization Code**: Single-use, expires in 10 minutes
44
+ - **Token Hashing**: All tokens stored as SHA-256 hashes
45
+ - **Refresh Token Rotation**: New refresh token issued on each use, old token invalidated
46
+
47
+ ## Security Features
48
+
49
+ ### Built-in Security Controls
50
+
51
+ - **PKCE Enforcement**: All authorization code flows require PKCE (code_challenge/code_verifier)
52
+ - **Redirect URI Validation**: Strict checking against registered URIs
53
+ - **Scope Validation**: Only registered scopes are allowed per client
54
+ - **Token Hashing**: Access and refresh tokens are stored as SHA-256 hashes
55
+ - **Client Secret Hashing**: Confidential client secrets use bcrypt
56
+ - **Internal Mutations**: Critical operations like `issueAuthorizationCode` are not directly accessible
57
+ - **DCR Disabled by Default**: Dynamic Client Registration must be explicitly enabled
58
+
59
+ ### Authorization Flow Security
60
+
61
+ The `/oauth/authorize` endpoint performs comprehensive validation:
62
+ 1. Client ID verification
63
+ 2. Redirect URI matching against registered URIs
64
+ 3. Scope validation against client's allowed scopes
65
+ 4. PKCE requirement (code_challenge with S256 method)
66
+ 5. User authentication via `getUserId` hook
67
+
68
+ ## Scopes and Token Types
69
+
70
+ ### Supported Scopes
71
+
72
+ - **`openid`**: Required for OpenID Connect authentication and ID tokens
73
+ - **`profile`**: Grants access to user profile information (name, picture)
74
+ - **`email`**: Grants access to user email address
75
+ - **`offline_access`**: Enables refresh token issuance for long-lived access
76
+
77
+ ### Refresh Token Requirements
78
+
79
+ Refresh tokens are **only issued** when the `offline_access` scope is requested and granted during the initial authorization:
80
+
81
+ - ✅ **With `offline_access`**: Client receives both access token and refresh token
82
+ - ❌ **Without `offline_access`**: Client receives only access token (no refresh token)
83
+
84
+ **Refresh Token Grant Flow:**
85
+ - Use the `refresh_token` grant type to obtain new access tokens
86
+ - The original authorization must have included the `offline_access` scope
87
+ - Refresh tokens are automatically rotated on each use (old token is invalidated)
88
+ - The new refresh token maintains the same scope as the original
89
+
90
+ This follows OAuth 2.1 and OpenID Connect specifications, ensuring that long-lived refresh tokens are only issued with explicit user consent.
91
+
92
+ ## OAuth Token Detection Helper
93
+
94
+ Provides helper functions to distinguish between OAuth tokens and session tokens:
95
+
96
+ ```typescript
97
+ import { isOAuthToken, getOAuthClientId } from "@codefox-inc/oauth-provider";
98
+
99
+ const identity = await ctx.auth.getUserIdentity();
100
+
101
+ if (isOAuthToken(identity)) {
102
+ // Handle OAuth token (MCP client, third-party apps, etc.)
103
+ const clientId = getOAuthClientId(identity);
104
+ console.log("OAuth client:", clientId);
105
+ } else {
106
+ // Handle Convex Auth session (first-party user)
107
+ }
108
+ ```
109
+
110
+ ## Setup
111
+
112
+ ### 1. Set Environment Variables
113
+
114
+ #### With Convex Auth (Recommended)
115
+
116
+ If you're using [Convex Auth](https://labs.convex.dev/auth), run the setup command:
117
+
118
+ ```bash
119
+ npx @convex-dev/auth
120
+ ```
121
+
122
+ This automatically sets `JWT_PRIVATE_KEY`, `JWKS`, and `SITE_URL`. The OAuth Provider will use these by default.
123
+
124
+ #### Without Convex Auth
125
+
126
+ Generate RSA keys manually:
127
+
128
+ ```bash
129
+ # Generate private key
130
+ openssl genrsa -out private.pem 2048
131
+
132
+ # Generate JWKS (use https://mkjwk.org or this script)
133
+ node -e "
134
+ const jose = require('jose');
135
+ const fs = require('fs');
136
+ const privateKey = fs.readFileSync('private.pem', 'utf8');
137
+ (async () => {
138
+ const key = await jose.importPKCS8(privateKey, 'RS256');
139
+ const jwk = await jose.exportJWK(key);
140
+ console.log(JSON.stringify({ keys: [{ ...jwk, use: 'sig', alg: 'RS256', kid: 'default-key' }] }));
141
+ })();
142
+ "
143
+ ```
144
+
145
+ Set environment variables:
146
+
147
+ ```bash
148
+ npx convex env set OAUTH_PRIVATE_KEY "-----BEGIN RSA PRIVATE KEY-----\n..."
149
+ npx convex env set OAUTH_JWKS '{"keys":[...]}'
150
+ npx convex env set SITE_URL "https://your-app.example.com"
151
+ ```
152
+
153
+ ### 2. Register Component
154
+
155
+ ```typescript
156
+ // convex/convex.config.ts
157
+ import { defineApp } from "convex/server";
158
+ import oauthProvider from "@codefox-inc/oauth-provider/convex.config";
159
+
160
+ const app = defineApp();
161
+ app.use(oauthProvider, { name: "oauthProvider" });
162
+
163
+ export default app;
164
+ ```
165
+
166
+ ### 3. Configure HTTP Routes
167
+
168
+ #### Option A: Using the Helper Function (Recommended)
169
+
170
+ ```typescript
171
+ // convex/http.ts
172
+ import { httpAction } from "./_generated/server";
173
+ import { httpRouter } from "convex/server";
174
+ import { OAuthProvider, registerOAuthRoutes } from "@codefox-inc/oauth-provider";
175
+ import { components } from "./_generated/api";
176
+ import { api } from "./_generated/api";
177
+
178
+ const http = httpRouter();
179
+
180
+ const oauthProvider = new OAuthProvider(components.oauthProvider, {
181
+ privateKey: process.env.OAUTH_PRIVATE_KEY!,
182
+ jwks: process.env.OAUTH_JWKS!,
183
+ siteUrl: process.env.SITE_URL!,
184
+ convexSiteUrl: process.env.CONVEX_SITE_URL,
185
+ // OPTIONAL: OAuth endpoint prefix (default: "/oauth")
186
+ // Note: This must match the route prefix used below.
187
+ // prefix: "/oauth",
188
+ allowedScopes: ["openid", "profile", "email", "offline_access"],
189
+
190
+ // REQUIRED: Authenticate user for authorization endpoint
191
+ getUserId: async (ctx, request) => {
192
+ const identity = await ctx.auth.getUserIdentity();
193
+ return identity?.subject ?? null;
194
+ },
195
+
196
+ // OPTIONAL: Enable dynamic client registration (default: false)
197
+ allowDynamicClientRegistration: false,
198
+ });
199
+
200
+ // Register all OAuth routes automatically
201
+ registerOAuthRoutes(http, httpAction, oauthProvider, {
202
+ siteUrl: process.env.SITE_URL!,
203
+ // OPTIONAL: Override the prefix used for route registration.
204
+ // By default, this uses oauthProvider's config prefix.
205
+ // prefix: "/oauth",
206
+ getUserProfile: async (ctx, userId) => {
207
+ // Return user profile for /oauth/userinfo endpoint
208
+ const user = await ctx.runQuery(api.users.get, { userId });
209
+ return user ? {
210
+ sub: userId,
211
+ name: user.name,
212
+ email: user.email,
213
+ picture: user.pictureUrl
214
+ } : null;
215
+ },
216
+ });
217
+
218
+ export default http;
219
+ ```
220
+
221
+ #### Option B: Manual Route Registration
222
+
223
+ ```typescript
224
+ // convex/http.ts
225
+ import { httpAction } from "./_generated/server";
226
+ import { httpRouter } from "convex/server";
227
+ import { OAuthProvider } from "@codefox-inc/oauth-provider";
228
+ import { components } from "./_generated/api";
229
+
230
+ const http = httpRouter();
231
+
232
+ const oauthProvider = new OAuthProvider(components.oauthProvider, {
233
+ privateKey: process.env.OAUTH_PRIVATE_KEY!,
234
+ jwks: process.env.OAUTH_JWKS!,
235
+ siteUrl: process.env.SITE_URL!,
236
+ convexSiteUrl: process.env.CONVEX_SITE_URL,
237
+ // OPTIONAL: OAuth endpoint prefix (default: "/oauth")
238
+ // Note: This must match the route prefix used below.
239
+ // prefix: "/oauth",
240
+ allowedScopes: ["openid", "profile", "email", "offline_access"],
241
+
242
+ // REQUIRED: Authenticate user for authorization endpoint
243
+ getUserId: async (ctx, request) => {
244
+ const identity = await ctx.auth.getUserIdentity();
245
+ return identity?.subject ?? null;
246
+ },
247
+ });
248
+
249
+ // OpenID Connect Discovery
250
+ http.route({
251
+ path: "/oauth/.well-known/openid-configuration",
252
+ method: "GET",
253
+ handler: httpAction((ctx, req) =>
254
+ oauthProvider.handlers.openIdConfiguration(ctx, req)
255
+ ),
256
+ });
257
+
258
+ // JWKS endpoint
259
+ http.route({
260
+ path: "/oauth/.well-known/jwks.json",
261
+ method: "GET",
262
+ handler: httpAction((ctx, req) =>
263
+ oauthProvider.handlers.jwks(ctx, req)
264
+ ),
265
+ });
266
+
267
+ // Authorization endpoint (validates and issues auth codes)
268
+ http.route({
269
+ path: "/oauth/authorize",
270
+ method: "GET",
271
+ handler: httpAction((ctx, req) =>
272
+ oauthProvider.handlers.authorize(ctx, req)
273
+ ),
274
+ });
275
+
276
+ // Token endpoint
277
+ http.route({
278
+ path: "/oauth/token",
279
+ method: "POST",
280
+ handler: httpAction((ctx, req) =>
281
+ oauthProvider.handlers.token(ctx, req)
282
+ ),
283
+ });
284
+
285
+ // UserInfo endpoint
286
+ http.route({
287
+ path: "/oauth/userinfo",
288
+ method: "GET",
289
+ handler: httpAction((ctx, req) =>
290
+ oauthProvider.handlers.userInfo(ctx, req, async (userId) => {
291
+ const user = await ctx.runQuery(api.users.get, { userId });
292
+ return user ? { sub: userId, name: user.name, email: user.email } : null;
293
+ })
294
+ ),
295
+ });
296
+
297
+ // Dynamic Client Registration (optional)
298
+ http.route({
299
+ path: "/oauth/register",
300
+ method: "POST",
301
+ handler: httpAction((ctx, req) =>
302
+ oauthProvider.handlers.register(ctx, req)
303
+ ),
304
+ });
305
+
306
+ export default http;
307
+ ```
308
+
309
+ ## UserInfo Endpoint
310
+
311
+ Requires `openid` scope. Returns claims based on scopes:
312
+ - `openid`: Always returns `sub`
313
+ - `profile`: Adds `name`, `picture`
314
+ - `email`: Adds `email` (and `email_verified` if available)
315
+
316
+ ## Client Registration
317
+
318
+ ### Register OAuth Client (Admin)
319
+
320
+ ```typescript
321
+ // convex/oauthAdmin.ts
322
+ import { mutation } from "./_generated/server";
323
+ import { OAuthProvider } from "@codefox-inc/oauth-provider";
324
+ import { components } from "./_generated/api";
325
+
326
+ export const registerOAuthClient = mutation({
327
+ handler: async (ctx, args: {
328
+ name: string;
329
+ redirectUris: string[];
330
+ scopes: string[];
331
+ type: "confidential" | "public";
332
+ }) => {
333
+ // Check admin permissions
334
+ const identity = await ctx.auth.getUserIdentity();
335
+ if (!identity) throw new Error("Unauthorized");
336
+
337
+ const oauthProvider = new OAuthProvider(components.oauthProvider, {
338
+ privateKey: process.env.OAUTH_PRIVATE_KEY!,
339
+ jwks: process.env.OAUTH_JWKS!,
340
+ siteUrl: process.env.SITE_URL!,
341
+ });
342
+
343
+ const result = await oauthProvider.registerClient(ctx, {
344
+ name: args.name,
345
+ redirectUris: args.redirectUris,
346
+ scopes: args.scopes,
347
+ type: args.type,
348
+ });
349
+
350
+ // IMPORTANT: Save clientSecret securely - it's only returned once!
351
+ return result;
352
+ },
353
+ });
354
+ ```
355
+
356
+ ## Authorization Flow
357
+
358
+ ### Automatic Authorization Handler
359
+
360
+ The `/oauth/authorize` endpoint handles the complete authorization flow automatically:
361
+
362
+ ```
363
+ GET /oauth/authorize?
364
+ response_type=code
365
+ &client_id=CLIENT_ID
366
+ &redirect_uri=REDIRECT_URI
367
+ &scope=openid+profile+email
368
+ &state=STATE
369
+ &code_challenge=CHALLENGE
370
+ &code_challenge_method=S256
371
+ &nonce=NONCE
372
+ ```
373
+
374
+ The handler:
375
+ 1. Validates the client ID
376
+ 2. Checks redirect_uri against registered URIs
377
+ 3. Validates requested scopes
378
+ 4. Requires PKCE (code_challenge)
379
+ 5. Authenticates the user via `getUserId`
380
+ 6. Issues authorization code
381
+ 7. Redirects back to the client with the code
382
+
383
+ ### Custom Authorization Flow (Advanced)
384
+
385
+ If you need custom consent UI, you can use the SDK methods directly:
386
+
387
+ ```typescript
388
+ // convex/oauth.ts
389
+ import { mutation } from "./_generated/server";
390
+ import { OAuthProvider } from "@codefox-inc/oauth-provider";
391
+ import { components } from "./_generated/api";
392
+
393
+ export const approveAuthorization = mutation({
394
+ handler: async (ctx, args: {
395
+ clientId: string;
396
+ scopes: string[];
397
+ redirectUri: string;
398
+ codeChallenge: string;
399
+ codeChallengeMethod: string;
400
+ nonce?: string;
401
+ }) => {
402
+ // Verify user is authenticated
403
+ const identity = await ctx.auth.getUserIdentity();
404
+ if (!identity) throw new Error("Not authenticated");
405
+
406
+ const oauthProvider = new OAuthProvider(components.oauthProvider, {
407
+ privateKey: process.env.OAUTH_PRIVATE_KEY!,
408
+ jwks: process.env.OAUTH_JWKS!,
409
+ siteUrl: process.env.SITE_URL!,
410
+ });
411
+
412
+ // Issue authorization code (automatically creates authorization record)
413
+ const authCode = await oauthProvider.issueAuthorizationCode(ctx, {
414
+ userId: identity.subject,
415
+ clientId: args.clientId,
416
+ scopes: args.scopes,
417
+ redirectUri: args.redirectUri,
418
+ codeChallenge: args.codeChallenge,
419
+ codeChallengeMethod: args.codeChallengeMethod,
420
+ nonce: args.nonce,
421
+ });
422
+
423
+ return authCode;
424
+ },
425
+ });
426
+ ```
427
+
428
+ ## Authorization Management
429
+
430
+ ### List User's Authorized Apps
431
+
432
+ ```typescript
433
+ import { query } from "./_generated/server";
434
+ import { OAuthProvider } from "@codefox-inc/oauth-provider";
435
+ import { components } from "./_generated/api";
436
+
437
+ export const listAuthorizedApps = query({
438
+ handler: async (ctx) => {
439
+ const identity = await ctx.auth.getUserIdentity();
440
+ if (!identity) return [];
441
+
442
+ const oauthProvider = new OAuthProvider(components.oauthProvider, {
443
+ privateKey: process.env.OAUTH_PRIVATE_KEY!,
444
+ jwks: process.env.OAUTH_JWKS!,
445
+ siteUrl: process.env.SITE_URL!,
446
+ });
447
+
448
+ return await oauthProvider.listUserAuthorizations(ctx, identity.subject);
449
+ },
450
+ });
451
+ ```
452
+
453
+ ### Revoke Authorization
454
+
455
+ ```typescript
456
+ import { mutation } from "./_generated/server";
457
+ import { OAuthProvider } from "@codefox-inc/oauth-provider";
458
+ import { components } from "./_generated/api";
459
+
460
+ export const revokeApp = mutation({
461
+ handler: async (ctx, args: { clientId: string }) => {
462
+ const identity = await ctx.auth.getUserIdentity();
463
+ if (!identity) throw new Error("Not authenticated");
464
+
465
+ const oauthProvider = new OAuthProvider(components.oauthProvider, {
466
+ privateKey: process.env.OAUTH_PRIVATE_KEY!,
467
+ jwks: process.env.OAUTH_JWKS!,
468
+ siteUrl: process.env.SITE_URL!,
469
+ });
470
+
471
+ // Deletes authorization and all associated tokens
472
+ await oauthProvider.revokeAuthorization(ctx, identity.subject, args.clientId);
473
+ },
474
+ });
475
+ ```
476
+
477
+ ## Configuration Options
478
+
479
+ ### OAuthConfig
480
+
481
+ ```typescript
482
+ interface OAuthConfig {
483
+ // REQUIRED: RSA private key in PEM format
484
+ privateKey: string;
485
+
486
+ // REQUIRED: JWKS for token verification (public keys only)
487
+ jwks: string;
488
+
489
+ // REQUIRED: Your application URL
490
+ siteUrl: string;
491
+
492
+ // OPTIONAL: Convex deployment URL (if different from siteUrl)
493
+ convexSiteUrl?: string;
494
+
495
+ // OPTIONAL: OAuth endpoint prefix (default: "/oauth")
496
+ // Normalized to a leading slash, trailing slash removed; "/" means root.
497
+ // Must match the route prefix you register in http.ts.
498
+ prefix?: string;
499
+
500
+ // OPTIONAL: Comma-separated list of allowed CORS origins
501
+ allowedOrigins?: string;
502
+
503
+ // OPTIONAL: Allowed scopes for dynamic client registration
504
+ allowedScopes?: string[];
505
+
506
+ // REQUIRED: Function to get authenticated user ID
507
+ // Must return a Convex users table Id (string)
508
+ // Returns null if user is not authenticated
509
+ getUserId?: (ctx: ActionCtx, request: Request) => Promise<string | null> | string | null;
510
+
511
+ // OPTIONAL: Enable dynamic client registration (default: false)
512
+ allowDynamicClientRegistration?: boolean;
513
+ }
514
+ ```
515
+
516
+ ## Token Verification
517
+
518
+ ### In Convex Functions
519
+
520
+ ```typescript
521
+ import { query } from "./_generated/server";
522
+
523
+ export const protectedQuery = query({
524
+ handler: async (ctx) => {
525
+ const identity = await ctx.auth.getUserIdentity();
526
+ if (!identity) throw new Error("Not authenticated");
527
+
528
+ // Token is already verified by Convex Auth
529
+ // Use identity.subject for user ID
530
+ return { userId: identity.subject };
531
+ },
532
+ });
533
+ ```
534
+
535
+ ### External Token Verification
536
+
537
+ ```typescript
538
+ import { verifyAccessToken } from "@codefox-inc/oauth-provider";
539
+
540
+ const payload = await verifyAccessToken(
541
+ token,
542
+ {
543
+ jwks: process.env.OAUTH_JWKS!,
544
+ siteUrl: process.env.SITE_URL!,
545
+ },
546
+ issuerUrl
547
+ );
548
+
549
+ console.log("User ID:", payload.sub);
550
+ console.log("Scopes:", payload.scp);
551
+ console.log("Client ID:", payload.cid);
552
+ ```
553
+
554
+ ## Environment Variables Reference
555
+
556
+ | Variable | Required | Description |
557
+ |----------|----------|-------------|
558
+ | `OAUTH_PRIVATE_KEY` | Yes | RSA private key (PEM format) |
559
+ | `OAUTH_JWKS` | Yes | JSON Web Key Set for token verification |
560
+ | `SITE_URL` | Yes | Your application's public URL |
561
+ | `CONVEX_SITE_URL` | No | Convex deployment URL (used as issuer if set) |
562
+ | `ALLOWED_ORIGINS` | No | Comma-separated CORS origins |
563
+
564
+ ## Testing
565
+
566
+ ```bash
567
+ npm test
568
+ ```
569
+
570
+ ## License
571
+
572
+ Apache-2.0
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=_ignore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_ignore.d.ts","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // This is only here so convex-test can detect a _generated folder
3
+ //# sourceMappingURL=_ignore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_ignore.js","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":";AAAA,kEAAkE"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Auth Config Generator
3
+ *
4
+ * Generates auth.config.ts configuration for Convex Auth
5
+ * to trust JWTs from the OAuth Provider.
6
+ */
7
+ /**
8
+ * Auth provider configuration for Convex
9
+ */
10
+ export interface AuthProvider {
11
+ domain: string;
12
+ applicationID: string;
13
+ }
14
+ /**
15
+ * Auth config structure (matches Convex Auth config)
16
+ */
17
+ export interface AuthConfig {
18
+ providers: AuthProvider[];
19
+ }
20
+ /**
21
+ * Options for generating auth config
22
+ */
23
+ export interface GenerateAuthConfigOptions {
24
+ /**
25
+ * CONVEX_SITE_URL - the deployed Convex site URL
26
+ * @example "https://your-app.convex.site"
27
+ */
28
+ convexSiteUrl?: string;
29
+ /**
30
+ * Local development port for OAuth provider
31
+ * @default 5173
32
+ */
33
+ localPort?: number;
34
+ /**
35
+ * OAuth endpoint prefix
36
+ * @default "/oauth"
37
+ */
38
+ prefix?: string;
39
+ /**
40
+ * Audience value for JWT validation
41
+ * @default "convex"
42
+ */
43
+ applicationID?: string;
44
+ /**
45
+ * Additional provider domains to trust
46
+ */
47
+ additionalProviders?: AuthProvider[];
48
+ /**
49
+ * Include the CONVEX_SITE_URL as a provider (for Convex Auth)
50
+ * @default true
51
+ */
52
+ includeConvexSiteUrl?: boolean;
53
+ }
54
+ /**
55
+ * Generate auth.config.ts configuration for OAuth Provider
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // convex/auth.config.ts
60
+ * import { generateAuthConfig } from "@codefox-inc/oauth-provider";
61
+ *
62
+ * export default generateAuthConfig({
63
+ * convexSiteUrl: process.env.CONVEX_SITE_URL,
64
+ * localPort: 5173,
65
+ * });
66
+ * ```
67
+ *
68
+ * @example Output
69
+ * ```javascript
70
+ * {
71
+ * providers: [
72
+ * { domain: "https://your-app.convex.site", applicationID: "convex" },
73
+ * { domain: "http://localhost:5173/oauth", applicationID: "convex" },
74
+ * { domain: "https://your-app.convex.site/oauth", applicationID: "convex" },
75
+ * ]
76
+ * }
77
+ * ```
78
+ */
79
+ export declare function generateAuthConfig(options?: GenerateAuthConfigOptions): AuthConfig;
80
+ /**
81
+ * Create auth config with validation
82
+ * Throws if required environment variables are missing
83
+ */
84
+ export declare function createAuthConfig(options?: GenerateAuthConfigOptions): AuthConfig;
85
+ //# sourceMappingURL=auth-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-config.d.ts","sourceRoot":"","sources":["../../src/client/auth-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACvB,SAAS,EAAE,YAAY,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,mBAAmB,CAAC,EAAE,YAAY,EAAE,CAAC;IAErC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,UAAU,CAuCtF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,yBAA8B,GAAG,UAAU,CAsBpF"}