@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
@@ -0,0 +1,418 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { OAuthProvider } from "../index";
3
+
4
+ const component = {
5
+ queries: {
6
+ getClient: "getClient",
7
+ getRefreshToken: "getRefreshToken",
8
+ getTokensByUser: "getTokensByUser",
9
+ getAuthorization: "getAuthorization",
10
+ hasAuthorization: "hasAuthorization",
11
+ hasAnyAuthorization: "hasAnyAuthorization",
12
+ listUserAuthorizations: "listUserAuthorizations",
13
+ },
14
+ mutations: {
15
+ issueAuthorizationCode: "issueAuthorizationCode",
16
+ upsertAuthorization: "upsertAuthorization",
17
+ consumeAuthCode: "consumeAuthCode",
18
+ saveTokens: "saveTokens",
19
+ rotateRefreshToken: "rotateRefreshToken",
20
+ updateAuthorizationLastUsed: "updateAuthorizationLastUsed",
21
+ revokeAuthorization: "revokeAuthorization",
22
+ },
23
+ clientManagement: {
24
+ registerClient: "registerClient",
25
+ verifyClientSecret: "verifyClientSecret",
26
+ },
27
+ };
28
+
29
+ const config = {
30
+ privateKey: "key",
31
+ jwks: "{\"keys\":[{\"kty\":\"RSA\",\"n\":\"n\",\"e\":\"AQAB\"}]}",
32
+ siteUrl: "https://example.com",
33
+ };
34
+
35
+ describe("OAuthProvider", () => {
36
+ describe("constructor", () => {
37
+ test("should initialize with component and config", () => {
38
+ const provider = new OAuthProvider(component as any, config);
39
+ expect(provider).toBeDefined();
40
+ expect(provider.getConfig()).toEqual(config);
41
+ });
42
+ });
43
+
44
+ describe("getConfig", () => {
45
+ test("should return the config", () => {
46
+ const provider = new OAuthProvider(component as any, config);
47
+ expect(provider.getConfig()).toEqual(config);
48
+ });
49
+ });
50
+
51
+ describe("handlers", () => {
52
+ test("should expose all handler methods", () => {
53
+ const provider = new OAuthProvider(component as any, config);
54
+ expect(provider.handlers.openIdConfiguration).toBeTypeOf("function");
55
+ expect(provider.handlers.authorize).toBeTypeOf("function");
56
+ expect(provider.handlers.jwks).toBeTypeOf("function");
57
+ expect(provider.handlers.token).toBeTypeOf("function");
58
+ expect(provider.handlers.userInfo).toBeTypeOf("function");
59
+ expect(provider.handlers.register).toBeTypeOf("function");
60
+ expect(provider.handlers.protectedResource).toBeTypeOf("function");
61
+ });
62
+ });
63
+
64
+ describe("issueAuthorizationCode", () => {
65
+ test("requires codeChallenge", async () => {
66
+ const provider = new OAuthProvider(component as any, config);
67
+ const ctx = { runMutation: vi.fn() };
68
+
69
+ await expect(provider.issueAuthorizationCode(ctx as any, {
70
+ userId: "user-1",
71
+ clientId: "client-1",
72
+ scopes: ["openid"],
73
+ redirectUri: "https://cb",
74
+ })).rejects.toThrow("codeChallenge required");
75
+ });
76
+
77
+ test("requires S256 codeChallengeMethod", async () => {
78
+ const provider = new OAuthProvider(component as any, config);
79
+ const ctx = { runMutation: vi.fn() };
80
+
81
+ await expect(provider.issueAuthorizationCode(ctx as any, {
82
+ userId: "user-1",
83
+ clientId: "client-1",
84
+ scopes: ["openid"],
85
+ redirectUri: "https://cb",
86
+ codeChallenge: "challenge",
87
+ codeChallengeMethod: "plain",
88
+ })).rejects.toThrow("codeChallengeMethod must be S256");
89
+ });
90
+
91
+ test("defaults codeChallengeMethod to S256", async () => {
92
+ const provider = new OAuthProvider(component as any, config);
93
+ const runMutation = vi.fn(async (mutationRef: string, _args: unknown) => {
94
+ if (mutationRef === component.mutations.issueAuthorizationCode) {
95
+ return "code";
96
+ }
97
+ if (mutationRef === component.mutations.upsertAuthorization) {
98
+ return "auth";
99
+ }
100
+ return undefined;
101
+ });
102
+ const ctx = { runMutation };
103
+
104
+ const code = await provider.issueAuthorizationCode(ctx as any, {
105
+ userId: "user-1",
106
+ clientId: "client-1",
107
+ scopes: ["openid"],
108
+ redirectUri: "https://cb",
109
+ codeChallenge: "challenge",
110
+ });
111
+
112
+ expect(code).toBe("code");
113
+ expect(runMutation).toHaveBeenCalledWith(component.mutations.upsertAuthorization, {
114
+ userId: "user-1",
115
+ clientId: "client-1",
116
+ scopes: ["openid"],
117
+ });
118
+ expect(runMutation).toHaveBeenCalledWith(component.mutations.issueAuthorizationCode, {
119
+ userId: "user-1",
120
+ clientId: "client-1",
121
+ scopes: ["openid"],
122
+ redirectUri: "https://cb",
123
+ codeChallenge: "challenge",
124
+ codeChallengeMethod: "S256",
125
+ });
126
+ });
127
+ });
128
+
129
+ describe("API methods", () => {
130
+ test("getClient should call queries.getClient with clientId", async () => {
131
+ const provider = new OAuthProvider(component as any, config);
132
+ const runQuery = vi.fn(async () => ({ clientId: "client-1", name: "Test Client" }));
133
+ const ctx = { runQuery };
134
+
135
+ const result = await provider.getClient(ctx as any, "client-1");
136
+
137
+ expect(result).toEqual({ clientId: "client-1", name: "Test Client" });
138
+ expect(runQuery).toHaveBeenCalledWith(component.queries.getClient, { clientId: "client-1" });
139
+ });
140
+
141
+ test("API getRefreshToken should be callable", async () => {
142
+ const provider = new OAuthProvider(component as any, config);
143
+ const runQuery = vi.fn(async () => ({ refreshToken: "token" }));
144
+ const ctx = { runQuery };
145
+
146
+ // Access the internal API
147
+ const api = (provider as any).api;
148
+ await api.queries.getRefreshToken(ctx, { refreshToken: "token-hash" });
149
+
150
+ expect(runQuery).toHaveBeenCalledWith(component.queries.getRefreshToken, { refreshToken: "token-hash" });
151
+ });
152
+
153
+ test("API getTokensByUser should be callable", async () => {
154
+ const provider = new OAuthProvider(component as any, config);
155
+ const runQuery = vi.fn(async () => [{ accessToken: "token" }]);
156
+ const ctx = { runQuery };
157
+
158
+ const api = (provider as any).api;
159
+ await api.queries.getTokensByUser(ctx, { userId: "user-1" });
160
+
161
+ expect(runQuery).toHaveBeenCalledWith(component.queries.getTokensByUser, { userId: "user-1" });
162
+ });
163
+
164
+ test("API consumeAuthCode should be callable", async () => {
165
+ const provider = new OAuthProvider(component as any, config);
166
+ const runMutation = vi.fn(async () => ({ userId: "user-1", scopes: ["openid"] }));
167
+ const ctx = { runMutation };
168
+
169
+ const api = (provider as any).api;
170
+ await api.mutations.consumeAuthCode(ctx, {
171
+ code: "code",
172
+ clientId: "client-1",
173
+ redirectUri: "https://cb",
174
+ codeVerifier: "verifier"
175
+ });
176
+
177
+ expect(runMutation).toHaveBeenCalledWith(component.mutations.consumeAuthCode, {
178
+ code: "code",
179
+ clientId: "client-1",
180
+ redirectUri: "https://cb",
181
+ codeVerifier: "verifier"
182
+ });
183
+ });
184
+
185
+ test("API saveTokens should be callable", async () => {
186
+ const provider = new OAuthProvider(component as any, config);
187
+ const runMutation = vi.fn(async () => undefined);
188
+ const ctx = { runMutation };
189
+
190
+ const api = (provider as any).api;
191
+ await api.mutations.saveTokens(ctx, {
192
+ accessToken: "token",
193
+ clientId: "client-1",
194
+ userId: "user-1",
195
+ scopes: ["openid"],
196
+ expiresAt: Date.now() + 3600000
197
+ });
198
+
199
+ expect(runMutation).toHaveBeenCalled();
200
+ });
201
+
202
+ test("API rotateRefreshToken should be callable", async () => {
203
+ const provider = new OAuthProvider(component as any, config);
204
+ const runMutation = vi.fn(async () => undefined);
205
+ const ctx = { runMutation };
206
+
207
+ const api = (provider as any).api;
208
+ await api.mutations.rotateRefreshToken(ctx, {
209
+ oldRefreshToken: "old",
210
+ accessToken: "new-at",
211
+ clientId: "client-1",
212
+ userId: "user-1",
213
+ scopes: ["openid"],
214
+ expiresAt: Date.now() + 3600000
215
+ });
216
+
217
+ expect(runMutation).toHaveBeenCalled();
218
+ });
219
+
220
+ test("API updateAuthorizationLastUsed should be callable", async () => {
221
+ const provider = new OAuthProvider(component as any, config);
222
+ const runMutation = vi.fn(async () => undefined);
223
+ const ctx = { runMutation };
224
+
225
+ const api = (provider as any).api;
226
+ await api.mutations.updateAuthorizationLastUsed(ctx, {
227
+ userId: "user-1",
228
+ clientId: "client-1"
229
+ });
230
+
231
+ expect(runMutation).toHaveBeenCalled();
232
+ });
233
+
234
+ test("API verifyClientSecret should be callable", async () => {
235
+ const provider = new OAuthProvider(component as any, config);
236
+ const runMutation = vi.fn(async () => true);
237
+ const ctx = { runMutation };
238
+
239
+ const api = (provider as any).api;
240
+ const result = await api.clientManagement.verifyClientSecret(ctx, {
241
+ clientId: "client-1",
242
+ clientSecret: "secret"
243
+ });
244
+
245
+ expect(result).toBe(true);
246
+ expect(runMutation).toHaveBeenCalled();
247
+ });
248
+ });
249
+
250
+ describe("registerClient", () => {
251
+ test("should call clientManagement.registerClient", async () => {
252
+ const provider = new OAuthProvider(component as any, config);
253
+ const runMutation = vi.fn(async () => ({ clientId: "new-client", clientSecret: "secret" }));
254
+ const ctx = { runMutation };
255
+
256
+ const result = await provider.registerClient(ctx as any, {
257
+ name: "New Client",
258
+ redirectUris: ["https://example.com/callback"],
259
+ scopes: ["openid"],
260
+ type: "confidential",
261
+ });
262
+
263
+ expect(result).toEqual({ clientId: "new-client", clientSecret: "secret" });
264
+ expect(runMutation).toHaveBeenCalledWith(component.clientManagement.registerClient, {
265
+ name: "New Client",
266
+ redirectUris: ["https://example.com/callback"],
267
+ scopes: ["openid"],
268
+ type: "confidential",
269
+ });
270
+ });
271
+ });
272
+
273
+ describe("getTokensByUser", () => {
274
+ test("should call queries.getTokensByUser with userId", async () => {
275
+ const provider = new OAuthProvider(component as any, config);
276
+ const runQuery = vi.fn(async () => [{ accessToken: "token1" }]);
277
+ const ctx = { runQuery };
278
+
279
+ const result = await provider.getTokensByUser(ctx as any, "user-1");
280
+
281
+ expect(result).toEqual([{ accessToken: "token1" }]);
282
+ expect(runQuery).toHaveBeenCalledWith(component.queries.getTokensByUser, { userId: "user-1" });
283
+ });
284
+ });
285
+
286
+ describe("getAuthorization", () => {
287
+ test("should call queries.getAuthorization", async () => {
288
+ const provider = new OAuthProvider(component as any, config);
289
+ const runQuery = vi.fn(async () => ({ userId: "user-1", clientId: "client-1", scopes: ["openid"] }));
290
+ const ctx = { runQuery };
291
+
292
+ const result = await provider.getAuthorization(ctx as any, "user-1", "client-1");
293
+
294
+ expect(result).toEqual({ userId: "user-1", clientId: "client-1", scopes: ["openid"] });
295
+ expect(runQuery).toHaveBeenCalledWith(component.queries.getAuthorization, { userId: "user-1", clientId: "client-1" });
296
+ });
297
+ });
298
+
299
+ describe("listUserAuthorizations", () => {
300
+ test("should call queries.listUserAuthorizations", async () => {
301
+ const provider = new OAuthProvider(component as any, config);
302
+ const runQuery = vi.fn(async () => [{ clientId: "client-1", scopes: ["openid"] }]);
303
+ const ctx = { runQuery };
304
+
305
+ const result = await provider.listUserAuthorizations(ctx as any, "user-1");
306
+
307
+ expect(result).toEqual([{ clientId: "client-1", scopes: ["openid"] }]);
308
+ expect(runQuery).toHaveBeenCalledWith(component.queries.listUserAuthorizations, { userId: "user-1" });
309
+ });
310
+ });
311
+
312
+ describe("upsertAuthorization", () => {
313
+ test("should call mutations.upsertAuthorization", async () => {
314
+ const provider = new OAuthProvider(component as any, config);
315
+ const runMutation = vi.fn(async () => "auth-id");
316
+ const ctx = { runMutation };
317
+
318
+ const result = await provider.upsertAuthorization(ctx as any, {
319
+ userId: "user-1",
320
+ clientId: "client-1",
321
+ scopes: ["openid"],
322
+ });
323
+
324
+ expect(result).toBe("auth-id");
325
+ expect(runMutation).toHaveBeenCalledWith(component.mutations.upsertAuthorization, {
326
+ userId: "user-1",
327
+ clientId: "client-1",
328
+ scopes: ["openid"],
329
+ });
330
+ });
331
+ });
332
+
333
+ describe("revokeAuthorization", () => {
334
+ test("should call mutations.revokeAuthorization", async () => {
335
+ const provider = new OAuthProvider(component as any, config);
336
+ const runMutation = vi.fn(async () => undefined);
337
+ const ctx = { runMutation };
338
+
339
+ await provider.revokeAuthorization(ctx as any, "user-1", "client-1");
340
+
341
+ expect(runMutation).toHaveBeenCalledWith(component.mutations.revokeAuthorization, {
342
+ userId: "user-1",
343
+ clientId: "client-1",
344
+ });
345
+ });
346
+ });
347
+
348
+ describe("hasAuthorization", () => {
349
+ test("should return false when authorization does not exist", async () => {
350
+ const provider = new OAuthProvider(component as any, config);
351
+ const runQuery = vi.fn(async () => null);
352
+ const ctx = { runQuery };
353
+
354
+ const result = await provider.hasAuthorization(ctx as any, "user-1", "client-1", ["openid"]);
355
+
356
+ expect(result).toBe(false);
357
+ });
358
+
359
+ test("should return false when scopes are insufficient", async () => {
360
+ const provider = new OAuthProvider(component as any, config);
361
+ const runQuery = vi.fn(async () => ({ scopes: ["openid"] }));
362
+ const ctx = { runQuery };
363
+
364
+ const result = await provider.hasAuthorization(ctx as any, "user-1", "client-1", ["openid", "email"]);
365
+
366
+ expect(result).toBe(false);
367
+ });
368
+
369
+ test("should return true when all scopes are authorized", async () => {
370
+ const provider = new OAuthProvider(component as any, config);
371
+ const runQuery = vi.fn(async () => ({ scopes: ["openid", "email", "profile"] }));
372
+ const ctx = { runQuery };
373
+
374
+ const result = await provider.hasAuthorization(ctx as any, "user-1", "client-1", ["openid", "email"]);
375
+
376
+ expect(result).toBe(true);
377
+ });
378
+ });
379
+
380
+ describe("checkAuthorizationValid", () => {
381
+ test("should check specific client authorization when clientId provided", async () => {
382
+ const provider = new OAuthProvider(component as any, config);
383
+ const runQuery = vi.fn(async () => true);
384
+ const ctx = { runQuery };
385
+
386
+ const result = await provider.checkAuthorizationValid(ctx as any, "user-1", "client-1");
387
+
388
+ expect(result).toBe(true);
389
+ expect(runQuery).toHaveBeenCalledWith(component.queries.hasAuthorization, { userId: "user-1", clientId: "client-1" });
390
+ });
391
+
392
+ test("should check any authorization when clientId not provided", async () => {
393
+ const provider = new OAuthProvider(component as any, config);
394
+ const runQuery = vi.fn(async () => true);
395
+ const ctx = { runQuery };
396
+
397
+ const result = await provider.checkAuthorizationValid(ctx as any, "user-1");
398
+
399
+ expect(result).toBe(true);
400
+ expect(runQuery).toHaveBeenCalledWith(component.queries.hasAnyAuthorization, { userId: "user-1" });
401
+ });
402
+ });
403
+
404
+ describe("createAuthorizationChecker", () => {
405
+ test("should return a function that checks authorization", async () => {
406
+ const provider = new OAuthProvider(component as any, config);
407
+ const runQuery = vi.fn(async () => true);
408
+ const ctx = { runQuery };
409
+
410
+ const checker = provider.createAuthorizationChecker();
411
+ const result = await checker(ctx as any, "user-1", "client-1");
412
+
413
+ expect(result).toBe(true);
414
+ expect(runQuery).toHaveBeenCalledWith(component.queries.hasAuthorization, { userId: "user-1", clientId: "client-1" });
415
+ });
416
+ });
417
+
418
+ });