@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.
- package/LICENSE +201 -0
- package/README.md +572 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/auth-config.d.ts +85 -0
- package/dist/client/auth-config.d.ts.map +1 -0
- package/dist/client/auth-config.js +81 -0
- package/dist/client/auth-config.js.map +1 -0
- package/dist/client/auth-helper.d.ts +81 -0
- package/dist/client/auth-helper.d.ts.map +1 -0
- package/dist/client/auth-helper.js +97 -0
- package/dist/client/auth-helper.js.map +1 -0
- package/dist/client/index.d.ts +189 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +230 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/routes.d.ts +94 -0
- package/dist/client/routes.d.ts.map +1 -0
- package/dist/client/routes.js +113 -0
- package/dist/client/routes.js.map +1 -0
- package/dist/component/_generated/api.d.ts +44 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +123 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/clientManagement.d.ts +39 -0
- package/dist/component/clientManagement.d.ts.map +1 -0
- package/dist/component/clientManagement.js +169 -0
- package/dist/component/clientManagement.js.map +1 -0
- package/dist/component/constants.d.ts +31 -0
- package/dist/component/constants.d.ts.map +1 -0
- package/dist/component/constants.js +36 -0
- package/dist/component/constants.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/handlers.d.ts +143 -0
- package/dist/component/handlers.d.ts.map +1 -0
- package/dist/component/handlers.js +624 -0
- package/dist/component/handlers.js.map +1 -0
- package/dist/component/mutations.d.ts +111 -0
- package/dist/component/mutations.d.ts.map +1 -0
- package/dist/component/mutations.js +459 -0
- package/dist/component/mutations.js.map +1 -0
- package/dist/component/queries.d.ts +127 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +145 -0
- package/dist/component/queries.js.map +1 -0
- package/dist/component/schema.d.ts +116 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +77 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/token_security.d.ts +53 -0
- package/dist/component/token_security.d.ts.map +1 -0
- package/dist/component/token_security.js +91 -0
- package/dist/component/token_security.js.map +1 -0
- package/dist/lib/convex-types.d.ts +21 -0
- package/dist/lib/convex-types.d.ts.map +1 -0
- package/dist/lib/convex-types.js +2 -0
- package/dist/lib/convex-types.js.map +1 -0
- package/dist/lib/oauth.d.ts +123 -0
- package/dist/lib/oauth.d.ts.map +1 -0
- package/dist/lib/oauth.js +295 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +121 -0
- package/src/client/__tests__/auth-config.test.ts +244 -0
- package/src/client/__tests__/auth-helper.test.ts +273 -0
- package/src/client/__tests__/oauth-provider.test.ts +418 -0
- package/src/client/__tests__/routes.test.ts +428 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/auth-config.ts +157 -0
- package/src/client/auth-helper.ts +201 -0
- package/src/client/index.ts +326 -0
- package/src/client/routes.ts +251 -0
- package/src/component/__tests__/oauth.test.ts +3310 -0
- package/src/component/__tests__/rfc-compliance.test.ts +788 -0
- package/src/component/__tests__/token-security.test.ts +133 -0
- package/src/component/_generated/api.ts +60 -0
- package/src/component/_generated/component.ts +201 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/clientManagement.ts +189 -0
- package/src/component/constants.ts +40 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/handlers.ts +964 -0
- package/src/component/mutations.ts +531 -0
- package/src/component/queries.ts +165 -0
- package/src/component/schema.ts +92 -0
- package/src/component/token_security.ts +102 -0
- package/src/lib/__tests__/oauth-helpers.test.ts +143 -0
- package/src/lib/__tests__/oauth-jwt.test.ts +405 -0
- package/src/lib/convex-types.ts +37 -0
- package/src/lib/oauth.ts +412 -0
- package/src/react/index.ts +7 -0
- package/src/test.ts +21 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { registerOAuthRoutes } from "../routes";
|
|
3
|
+
|
|
4
|
+
describe("Route Registration", () => {
|
|
5
|
+
// Mock HTTP router
|
|
6
|
+
const createMockRouter = () => {
|
|
7
|
+
const routes: Array<{ path: string; method: string; handler: any }> = [];
|
|
8
|
+
return {
|
|
9
|
+
router: {
|
|
10
|
+
route: vi.fn((config: { path: string; method: string; handler: any }) => {
|
|
11
|
+
routes.push(config);
|
|
12
|
+
}),
|
|
13
|
+
},
|
|
14
|
+
routes,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mock httpAction creator
|
|
19
|
+
const mockHttpAction = (handler: any) => handler;
|
|
20
|
+
|
|
21
|
+
// Mock OAuth provider
|
|
22
|
+
const createMockProvider = (config?: { prefix?: string }) => {
|
|
23
|
+
const mockHandlers = {
|
|
24
|
+
openIdConfiguration: vi.fn(async () => new Response("{}")),
|
|
25
|
+
jwks: vi.fn(async () => new Response("{}")),
|
|
26
|
+
protectedResource: vi.fn(async () => new Response("{}")),
|
|
27
|
+
authorize: vi.fn(async () => new Response(null, { status: 302 })),
|
|
28
|
+
token: vi.fn(async () => new Response("{}")),
|
|
29
|
+
userInfo: vi.fn(async () => new Response("{}")),
|
|
30
|
+
register: vi.fn(async () => new Response("{}")),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
provider: {
|
|
35
|
+
handlers: mockHandlers,
|
|
36
|
+
getConfig: () => config,
|
|
37
|
+
},
|
|
38
|
+
mockHandlers,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
test("should register all OAuth endpoints with default prefix", () => {
|
|
43
|
+
const { router, routes } = createMockRouter();
|
|
44
|
+
const { provider } = createMockProvider();
|
|
45
|
+
|
|
46
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
47
|
+
|
|
48
|
+
// Expected routes (GET + OPTIONS for most, POST + OPTIONS for token/register/userinfo, GET + POST + OPTIONS for userinfo)
|
|
49
|
+
const expectedPaths = [
|
|
50
|
+
// Prefixed routes
|
|
51
|
+
"/oauth/.well-known/openid-configuration",
|
|
52
|
+
"/oauth/.well-known/oauth-authorization-server",
|
|
53
|
+
"/oauth/.well-known/jwks.json",
|
|
54
|
+
"/oauth/.well-known/oauth-protected-resource",
|
|
55
|
+
"/oauth/authorize",
|
|
56
|
+
"/oauth/token",
|
|
57
|
+
"/oauth/userinfo",
|
|
58
|
+
"/oauth/register",
|
|
59
|
+
// Root well-known routes
|
|
60
|
+
"/.well-known/oauth-authorization-server",
|
|
61
|
+
"/.well-known/oauth-authorization-server/oauth",
|
|
62
|
+
"/.well-known/oauth-protected-resource",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Check all expected paths are registered
|
|
66
|
+
const registeredPaths = routes.map(r => r.path);
|
|
67
|
+
for (const path of expectedPaths) {
|
|
68
|
+
expect(registeredPaths).toContain(path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Total routes: 11 paths × methods
|
|
72
|
+
// - 7 GET endpoints (openid-config, oauth-auth-server, jwks, protected-resource, authorize, userinfo, root×3) × 2 (GET + OPTIONS) = 14
|
|
73
|
+
// - 2 POST endpoints (token, register) × 2 (POST + OPTIONS) = 4
|
|
74
|
+
// - 1 userinfo (GET + POST + OPTIONS) = 3
|
|
75
|
+
expect(routes.length).toBeGreaterThan(20);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("should register routes with custom prefix", () => {
|
|
79
|
+
const { router, routes } = createMockRouter();
|
|
80
|
+
const { provider } = createMockProvider();
|
|
81
|
+
|
|
82
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
83
|
+
prefix: "/api/auth",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const registeredPaths = routes.map(r => r.path);
|
|
87
|
+
expect(registeredPaths).toContain("/api/auth/.well-known/openid-configuration");
|
|
88
|
+
expect(registeredPaths).toContain("/api/auth/authorize");
|
|
89
|
+
expect(registeredPaths).toContain("/api/auth/token");
|
|
90
|
+
expect(registeredPaths).toContain("/api/auth/userinfo");
|
|
91
|
+
expect(registeredPaths).toContain("/api/auth/register");
|
|
92
|
+
expect(registeredPaths).toContain("/.well-known/oauth-authorization-server/api/auth");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("should use prefix from provider config if not specified in options", () => {
|
|
96
|
+
const { router, routes } = createMockRouter();
|
|
97
|
+
const { provider } = createMockProvider({ prefix: "/custom" });
|
|
98
|
+
|
|
99
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
100
|
+
|
|
101
|
+
const registeredPaths = routes.map(r => r.path);
|
|
102
|
+
expect(registeredPaths).toContain("/custom/.well-known/openid-configuration");
|
|
103
|
+
expect(registeredPaths).toContain("/custom/authorize");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("should handle root prefix", () => {
|
|
107
|
+
const { router, routes } = createMockRouter();
|
|
108
|
+
const { provider } = createMockProvider();
|
|
109
|
+
|
|
110
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
111
|
+
prefix: "/",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const registeredPaths = routes.map(r => r.path);
|
|
115
|
+
expect(registeredPaths).toContain("/.well-known/openid-configuration");
|
|
116
|
+
expect(registeredPaths).toContain("/authorize");
|
|
117
|
+
expect(registeredPaths).toContain("/token");
|
|
118
|
+
expect(registeredPaths).toContain("/.well-known/oauth-authorization-server");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("should skip root well-known routes when disabled", () => {
|
|
122
|
+
const { router, routes } = createMockRouter();
|
|
123
|
+
const { provider } = createMockProvider();
|
|
124
|
+
|
|
125
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
126
|
+
registerRootWellKnown: false,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const registeredPaths = routes.map(r => r.path);
|
|
130
|
+
expect(registeredPaths).toContain("/oauth/.well-known/openid-configuration");
|
|
131
|
+
expect(registeredPaths).not.toContain("/.well-known/oauth-authorization-server");
|
|
132
|
+
expect(registeredPaths).not.toContain("/.well-known/oauth-protected-resource");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("should call custom authorizeHandler when provided", async () => {
|
|
136
|
+
const { router } = createMockRouter();
|
|
137
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
138
|
+
const customAuthorizeHandler = vi.fn(async (_ctx, _req, defaultFn) => {
|
|
139
|
+
return defaultFn();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
143
|
+
authorizeHandler: customAuthorizeHandler,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Find the authorize route handler
|
|
147
|
+
const authorizeRoute = router.route.mock.calls.find(
|
|
148
|
+
call => call[0].path === "/oauth/authorize" && call[0].method === "GET"
|
|
149
|
+
);
|
|
150
|
+
expect(authorizeRoute).toBeDefined();
|
|
151
|
+
|
|
152
|
+
// Invoke the handler
|
|
153
|
+
const handler = authorizeRoute![0].handler;
|
|
154
|
+
const mockCtx = { auth: { getUserIdentity: async () => null } };
|
|
155
|
+
const mockReq = new Request("http://localhost/oauth/authorize");
|
|
156
|
+
await handler(mockCtx, mockReq);
|
|
157
|
+
|
|
158
|
+
expect(customAuthorizeHandler).toHaveBeenCalled();
|
|
159
|
+
expect(mockHandlers.authorize).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("should use default getUserProfile when not provided", async () => {
|
|
163
|
+
const { router } = createMockRouter();
|
|
164
|
+
const { provider } = createMockProvider();
|
|
165
|
+
|
|
166
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
167
|
+
|
|
168
|
+
// Find userinfo route
|
|
169
|
+
const userInfoRoute = router.route.mock.calls.find(
|
|
170
|
+
call => call[0].path === "/oauth/userinfo" && call[0].method === "GET"
|
|
171
|
+
);
|
|
172
|
+
expect(userInfoRoute).toBeDefined();
|
|
173
|
+
|
|
174
|
+
// The handler should be registered
|
|
175
|
+
expect(userInfoRoute![0].handler).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should use custom getUserProfile when provided", async () => {
|
|
179
|
+
const { router } = createMockRouter();
|
|
180
|
+
const { provider } = createMockProvider();
|
|
181
|
+
const customGetUserProfile = vi.fn(async (_ctx, userId) => ({
|
|
182
|
+
sub: userId,
|
|
183
|
+
name: "Test User",
|
|
184
|
+
email: "test@example.com",
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
188
|
+
getUserProfile: customGetUserProfile,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Find userinfo route
|
|
192
|
+
const userInfoRoute = router.route.mock.calls.find(
|
|
193
|
+
call => call[0].path === "/oauth/userinfo" && call[0].method === "GET"
|
|
194
|
+
);
|
|
195
|
+
expect(userInfoRoute).toBeDefined();
|
|
196
|
+
|
|
197
|
+
// The handler should be registered
|
|
198
|
+
expect(userInfoRoute![0].handler).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("should register GET and OPTIONS for discovery endpoints", () => {
|
|
202
|
+
const { router, routes } = createMockRouter();
|
|
203
|
+
const { provider } = createMockProvider();
|
|
204
|
+
|
|
205
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
206
|
+
|
|
207
|
+
const openidConfigRoutes = routes.filter(
|
|
208
|
+
r => r.path === "/oauth/.well-known/openid-configuration"
|
|
209
|
+
);
|
|
210
|
+
expect(openidConfigRoutes).toHaveLength(2);
|
|
211
|
+
expect(openidConfigRoutes.map(r => r.method)).toEqual(
|
|
212
|
+
expect.arrayContaining(["GET", "OPTIONS"])
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("should register POST and OPTIONS for token endpoint", () => {
|
|
217
|
+
const { router, routes } = createMockRouter();
|
|
218
|
+
const { provider } = createMockProvider();
|
|
219
|
+
|
|
220
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
221
|
+
|
|
222
|
+
const tokenRoutes = routes.filter(r => r.path === "/oauth/token");
|
|
223
|
+
expect(tokenRoutes).toHaveLength(2);
|
|
224
|
+
expect(tokenRoutes.map(r => r.method)).toEqual(
|
|
225
|
+
expect.arrayContaining(["POST", "OPTIONS"])
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("should register GET, POST, and OPTIONS for userinfo endpoint", () => {
|
|
230
|
+
const { router, routes } = createMockRouter();
|
|
231
|
+
const { provider } = createMockProvider();
|
|
232
|
+
|
|
233
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
234
|
+
|
|
235
|
+
const userInfoRoutes = routes.filter(r => r.path === "/oauth/userinfo");
|
|
236
|
+
expect(userInfoRoutes).toHaveLength(3);
|
|
237
|
+
expect(userInfoRoutes.map(r => r.method)).toEqual(
|
|
238
|
+
expect.arrayContaining(["GET", "POST", "OPTIONS"])
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("should register routes with custom siteUrl", () => {
|
|
243
|
+
const { router } = createMockRouter();
|
|
244
|
+
const { provider } = createMockProvider();
|
|
245
|
+
|
|
246
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
247
|
+
siteUrl: "https://example.com",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// siteUrl is used internally but doesn't affect route registration
|
|
251
|
+
expect(router.route).toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("should handle all options combined", () => {
|
|
255
|
+
const { router, routes } = createMockRouter();
|
|
256
|
+
const { provider } = createMockProvider();
|
|
257
|
+
const customAuthorizeHandler = vi.fn(async (_ctx, _req, defaultFn) => defaultFn());
|
|
258
|
+
const customGetUserProfile = vi.fn(async (_ctx, userId) => ({ sub: userId }));
|
|
259
|
+
|
|
260
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
261
|
+
prefix: "/api/oauth",
|
|
262
|
+
getUserProfile: customGetUserProfile,
|
|
263
|
+
authorizeHandler: customAuthorizeHandler,
|
|
264
|
+
siteUrl: "https://example.com",
|
|
265
|
+
registerRootWellKnown: false,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const registeredPaths = routes.map(r => r.path);
|
|
269
|
+
expect(registeredPaths).toContain("/api/oauth/.well-known/openid-configuration");
|
|
270
|
+
expect(registeredPaths).toContain("/api/oauth/authorize");
|
|
271
|
+
expect(registeredPaths).toContain("/api/oauth/token");
|
|
272
|
+
expect(registeredPaths).toContain("/api/oauth/userinfo");
|
|
273
|
+
expect(registeredPaths).toContain("/api/oauth/register");
|
|
274
|
+
expect(registeredPaths).not.toContain("/.well-known/oauth-authorization-server");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("Handler Execution", () => {
|
|
278
|
+
test("should execute openIdConfiguration handler", async () => {
|
|
279
|
+
const { router, routes } = createMockRouter();
|
|
280
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
281
|
+
|
|
282
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
283
|
+
|
|
284
|
+
const openidRoute = routes.find(
|
|
285
|
+
r => r.path === "/oauth/.well-known/openid-configuration" && r.method === "GET"
|
|
286
|
+
);
|
|
287
|
+
expect(openidRoute).toBeDefined();
|
|
288
|
+
|
|
289
|
+
const mockCtx = {};
|
|
290
|
+
const mockReq = new Request("http://localhost/oauth/.well-known/openid-configuration");
|
|
291
|
+
await openidRoute!.handler(mockCtx, mockReq);
|
|
292
|
+
|
|
293
|
+
expect(mockHandlers.openIdConfiguration).toHaveBeenCalledWith(mockCtx, mockReq);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("should execute jwks handler", async () => {
|
|
297
|
+
const { router, routes } = createMockRouter();
|
|
298
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
299
|
+
|
|
300
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
301
|
+
|
|
302
|
+
const jwksRoute = routes.find(
|
|
303
|
+
r => r.path === "/oauth/.well-known/jwks.json" && r.method === "GET"
|
|
304
|
+
);
|
|
305
|
+
expect(jwksRoute).toBeDefined();
|
|
306
|
+
|
|
307
|
+
const mockCtx = {};
|
|
308
|
+
const mockReq = new Request("http://localhost/oauth/.well-known/jwks.json");
|
|
309
|
+
await jwksRoute!.handler(mockCtx, mockReq);
|
|
310
|
+
|
|
311
|
+
expect(mockHandlers.jwks).toHaveBeenCalledWith(mockCtx, mockReq);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("should execute protectedResource handler", async () => {
|
|
315
|
+
const { router, routes } = createMockRouter();
|
|
316
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
317
|
+
|
|
318
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
319
|
+
|
|
320
|
+
const protectedRoute = routes.find(
|
|
321
|
+
r => r.path === "/oauth/.well-known/oauth-protected-resource" && r.method === "GET"
|
|
322
|
+
);
|
|
323
|
+
expect(protectedRoute).toBeDefined();
|
|
324
|
+
|
|
325
|
+
const mockCtx = {};
|
|
326
|
+
const mockReq = new Request("http://localhost/oauth/.well-known/oauth-protected-resource");
|
|
327
|
+
await protectedRoute!.handler(mockCtx, mockReq);
|
|
328
|
+
|
|
329
|
+
expect(mockHandlers.protectedResource).toHaveBeenCalledWith(mockCtx, mockReq);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("should execute authorize handler without custom handler", async () => {
|
|
333
|
+
const { router, routes } = createMockRouter();
|
|
334
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
335
|
+
|
|
336
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
337
|
+
|
|
338
|
+
const authorizeRoute = routes.find(
|
|
339
|
+
r => r.path === "/oauth/authorize" && r.method === "GET"
|
|
340
|
+
);
|
|
341
|
+
expect(authorizeRoute).toBeDefined();
|
|
342
|
+
|
|
343
|
+
const mockCtx = {};
|
|
344
|
+
const mockReq = new Request("http://localhost/oauth/authorize");
|
|
345
|
+
await authorizeRoute!.handler(mockCtx, mockReq);
|
|
346
|
+
|
|
347
|
+
expect(mockHandlers.authorize).toHaveBeenCalledWith(mockCtx, mockReq);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("should execute token handler", async () => {
|
|
351
|
+
const { router, routes } = createMockRouter();
|
|
352
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
353
|
+
|
|
354
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
355
|
+
|
|
356
|
+
const tokenRoute = routes.find(
|
|
357
|
+
r => r.path === "/oauth/token" && r.method === "POST"
|
|
358
|
+
);
|
|
359
|
+
expect(tokenRoute).toBeDefined();
|
|
360
|
+
|
|
361
|
+
const mockCtx = {};
|
|
362
|
+
const mockReq = new Request("http://localhost/oauth/token", { method: "POST" });
|
|
363
|
+
await tokenRoute!.handler(mockCtx, mockReq);
|
|
364
|
+
|
|
365
|
+
expect(mockHandlers.token).toHaveBeenCalledWith(mockCtx, mockReq);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("should execute register handler", async () => {
|
|
369
|
+
const { router, routes } = createMockRouter();
|
|
370
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
371
|
+
|
|
372
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
373
|
+
|
|
374
|
+
const registerRoute = routes.find(
|
|
375
|
+
r => r.path === "/oauth/register" && r.method === "POST"
|
|
376
|
+
);
|
|
377
|
+
expect(registerRoute).toBeDefined();
|
|
378
|
+
|
|
379
|
+
const mockCtx = {};
|
|
380
|
+
const mockReq = new Request("http://localhost/oauth/register", { method: "POST" });
|
|
381
|
+
await registerRoute!.handler(mockCtx, mockReq);
|
|
382
|
+
|
|
383
|
+
expect(mockHandlers.register).toHaveBeenCalledWith(mockCtx, mockReq);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("should execute userInfo handler with default getUserProfile", async () => {
|
|
387
|
+
const { router, routes } = createMockRouter();
|
|
388
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
389
|
+
|
|
390
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
|
|
391
|
+
|
|
392
|
+
const userInfoRoute = routes.find(
|
|
393
|
+
r => r.path === "/oauth/userinfo" && r.method === "GET"
|
|
394
|
+
);
|
|
395
|
+
expect(userInfoRoute).toBeDefined();
|
|
396
|
+
|
|
397
|
+
const mockCtx = {};
|
|
398
|
+
const mockReq = new Request("http://localhost/oauth/userinfo");
|
|
399
|
+
await userInfoRoute!.handler(mockCtx, mockReq);
|
|
400
|
+
|
|
401
|
+
expect(mockHandlers.userInfo).toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("should execute userInfo handler with custom getUserProfile", async () => {
|
|
405
|
+
const { router, routes } = createMockRouter();
|
|
406
|
+
const { provider, mockHandlers } = createMockProvider();
|
|
407
|
+
const customGetUserProfile = vi.fn(async (_ctx, userId) => ({
|
|
408
|
+
sub: userId,
|
|
409
|
+
name: "Test User",
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
|
|
413
|
+
getUserProfile: customGetUserProfile,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const userInfoRoute = routes.find(
|
|
417
|
+
r => r.path === "/oauth/userinfo" && r.method === "GET"
|
|
418
|
+
);
|
|
419
|
+
expect(userInfoRoute).toBeDefined();
|
|
420
|
+
|
|
421
|
+
const mockCtx = {};
|
|
422
|
+
const mockReq = new Request("http://localhost/oauth/userinfo");
|
|
423
|
+
await userInfoRoute!.handler(mockCtx, mockReq);
|
|
424
|
+
|
|
425
|
+
expect(mockHandlers.userInfo).toHaveBeenCalled();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// This is only here so convex-test can detect a _generated folder
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
import { normalizePrefix } from "../lib/oauth.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Auth provider configuration for Convex
|
|
12
|
+
*/
|
|
13
|
+
export interface AuthProvider {
|
|
14
|
+
domain: string;
|
|
15
|
+
applicationID: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Auth config structure (matches Convex Auth config)
|
|
20
|
+
*/
|
|
21
|
+
export interface AuthConfig {
|
|
22
|
+
providers: AuthProvider[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for generating auth config
|
|
27
|
+
*/
|
|
28
|
+
export interface GenerateAuthConfigOptions {
|
|
29
|
+
/**
|
|
30
|
+
* CONVEX_SITE_URL - the deployed Convex site URL
|
|
31
|
+
* @example "https://your-app.convex.site"
|
|
32
|
+
*/
|
|
33
|
+
convexSiteUrl?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Local development port for OAuth provider
|
|
37
|
+
* @default 5173
|
|
38
|
+
*/
|
|
39
|
+
localPort?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* OAuth endpoint prefix
|
|
43
|
+
* @default "/oauth"
|
|
44
|
+
*/
|
|
45
|
+
prefix?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Audience value for JWT validation
|
|
49
|
+
* @default "convex"
|
|
50
|
+
*/
|
|
51
|
+
applicationID?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Additional provider domains to trust
|
|
55
|
+
*/
|
|
56
|
+
additionalProviders?: AuthProvider[];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Include the CONVEX_SITE_URL as a provider (for Convex Auth)
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
includeConvexSiteUrl?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate auth.config.ts configuration for OAuth Provider
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // convex/auth.config.ts
|
|
71
|
+
* import { generateAuthConfig } from "@codefox-inc/oauth-provider";
|
|
72
|
+
*
|
|
73
|
+
* export default generateAuthConfig({
|
|
74
|
+
* convexSiteUrl: process.env.CONVEX_SITE_URL,
|
|
75
|
+
* localPort: 5173,
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example Output
|
|
80
|
+
* ```javascript
|
|
81
|
+
* {
|
|
82
|
+
* providers: [
|
|
83
|
+
* { domain: "https://your-app.convex.site", applicationID: "convex" },
|
|
84
|
+
* { domain: "http://localhost:5173/oauth", applicationID: "convex" },
|
|
85
|
+
* { domain: "https://your-app.convex.site/oauth", applicationID: "convex" },
|
|
86
|
+
* ]
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function generateAuthConfig(options: GenerateAuthConfigOptions = {}): AuthConfig {
|
|
91
|
+
const {
|
|
92
|
+
convexSiteUrl,
|
|
93
|
+
localPort = 5173,
|
|
94
|
+
prefix: rawPrefix = "/oauth",
|
|
95
|
+
applicationID = "convex",
|
|
96
|
+
additionalProviders = [],
|
|
97
|
+
includeConvexSiteUrl = true,
|
|
98
|
+
} = options;
|
|
99
|
+
const prefix = normalizePrefix(rawPrefix);
|
|
100
|
+
|
|
101
|
+
const providers: AuthProvider[] = [];
|
|
102
|
+
|
|
103
|
+
// 1. CONVEX_SITE_URL for Convex Auth (session-based auth)
|
|
104
|
+
if (includeConvexSiteUrl && convexSiteUrl) {
|
|
105
|
+
providers.push({
|
|
106
|
+
domain: convexSiteUrl,
|
|
107
|
+
applicationID,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Local development OAuth issuer
|
|
112
|
+
providers.push({
|
|
113
|
+
domain: `http://localhost:${localPort}${prefix}`,
|
|
114
|
+
applicationID,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 3. Production OAuth issuer (CONVEX_SITE_URL + prefix)
|
|
118
|
+
if (convexSiteUrl) {
|
|
119
|
+
providers.push({
|
|
120
|
+
domain: `${convexSiteUrl}${prefix}`,
|
|
121
|
+
applicationID,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 4. Additional providers
|
|
126
|
+
providers.push(...additionalProviders);
|
|
127
|
+
|
|
128
|
+
return { providers };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create auth config with validation
|
|
133
|
+
* Throws if required environment variables are missing
|
|
134
|
+
*/
|
|
135
|
+
export function createAuthConfig(options: GenerateAuthConfigOptions = {}): AuthConfig {
|
|
136
|
+
const config = generateAuthConfig(options);
|
|
137
|
+
|
|
138
|
+
const prefix = normalizePrefix(options.prefix);
|
|
139
|
+
const localPort = options.localPort ?? 5173;
|
|
140
|
+
|
|
141
|
+
// Validate that we have at least one OAuth issuer (with the configured prefix/port)
|
|
142
|
+
const hasOAuthIssuer = config.providers.some(p => {
|
|
143
|
+
if (prefix) {
|
|
144
|
+
return p.domain.includes(prefix) || p.domain.includes(`:${localPort}`);
|
|
145
|
+
}
|
|
146
|
+
return p.domain.includes(`:${localPort}`) || (!!options.convexSiteUrl && p.domain === options.convexSiteUrl);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!hasOAuthIssuer) {
|
|
150
|
+
console.warn(
|
|
151
|
+
"[oauth-provider] Warning: No OAuth issuer found in auth config. " +
|
|
152
|
+
"MCP clients may not be able to authenticate."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return config;
|
|
157
|
+
}
|