openclaw-cloudflare 0.1.0 → 0.3.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/src/index.test.ts DELETED
@@ -1,395 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { describe, expect, it, vi, beforeEach } from "vitest";
3
-
4
- const createCloudflareAccessVerifierMock = vi.fn();
5
- const startGatewayCloudflareExposureMock = vi.fn();
6
-
7
- vi.mock("./tunnel/access.js", () => ({
8
- createCloudflareAccessVerifier: (...args: unknown[]) =>
9
- createCloudflareAccessVerifierMock(...args),
10
- }));
11
-
12
- vi.mock("./tunnel/exposure.js", () => ({
13
- startGatewayCloudflareExposure: (...args: unknown[]) =>
14
- startGatewayCloudflareExposureMock(...args),
15
- }));
16
-
17
- function createMockApi(pluginConfig?: Record<string, unknown>) {
18
- return {
19
- pluginConfig,
20
- logger: {
21
- info: vi.fn(),
22
- warn: vi.fn(),
23
- error: vi.fn(),
24
- },
25
- registerService: vi.fn(),
26
- registerHttpHandler: vi.fn(),
27
- };
28
- }
29
-
30
- function createMockReq(headers: Record<string, string | string[] | undefined> = {}): IncomingMessage {
31
- return { headers } as unknown as IncomingMessage;
32
- }
33
-
34
- function createMockRes(): ServerResponse {
35
- return {} as unknown as ServerResponse;
36
- }
37
-
38
- describe("cloudflare plugin", () => {
39
- beforeEach(() => {
40
- vi.restoreAllMocks();
41
- createCloudflareAccessVerifierMock.mockReset();
42
- startGatewayCloudflareExposureMock.mockReset();
43
- delete process.env.OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN;
44
- });
45
-
46
- it("does nothing when mode is off", async () => {
47
- const { default: plugin } = await import("./index.js");
48
- const api = createMockApi({ tunnel: { mode: "off" } });
49
-
50
- plugin.register(api);
51
-
52
- expect(api.registerService).not.toHaveBeenCalled();
53
- expect(api.registerHttpHandler).not.toHaveBeenCalled();
54
- });
55
-
56
- it("does nothing when no tunnel config (defaults to off)", async () => {
57
- const { default: plugin } = await import("./index.js");
58
- const api = createMockApi({});
59
-
60
- plugin.register(api);
61
-
62
- expect(api.registerService).not.toHaveBeenCalled();
63
- expect(api.registerHttpHandler).not.toHaveBeenCalled();
64
- });
65
-
66
- it("logs error and returns when managed mode has no token", async () => {
67
- const { default: plugin } = await import("./index.js");
68
- const api = createMockApi({ tunnel: { mode: "managed" } });
69
-
70
- plugin.register(api);
71
-
72
- expect(api.logger.error).toHaveBeenCalledWith(
73
- expect.stringContaining("managed mode requires tunnelToken"),
74
- );
75
- expect(api.registerService).not.toHaveBeenCalled();
76
- });
77
-
78
- it("reads tunnel token from env var when not in config", async () => {
79
- process.env.OPENCLAW_CLOUDFLARE_TUNNEL_TOKEN = "env-token";
80
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
81
-
82
- const { default: plugin } = await import("./index.js");
83
- const api = createMockApi({
84
- tunnel: { mode: "managed", teamDomain: "myteam" },
85
- });
86
-
87
- plugin.register(api);
88
-
89
- expect(api.registerService).toHaveBeenCalled();
90
- // Extract and run the service start to verify token is passed through
91
- const service = api.registerService.mock.calls[0][0];
92
- await service.start();
93
-
94
- expect(startGatewayCloudflareExposureMock).toHaveBeenCalledWith(
95
- expect.objectContaining({ tunnelToken: "env-token" }),
96
- );
97
- });
98
-
99
- it("warns when mode is active but no teamDomain is configured", async () => {
100
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
101
-
102
- const { default: plugin } = await import("./index.js");
103
- const api = createMockApi({ tunnel: { mode: "access-only" } });
104
-
105
- plugin.register(api);
106
-
107
- expect(api.logger.warn).toHaveBeenCalledWith(
108
- expect.stringContaining("no teamDomain configured"),
109
- );
110
- });
111
-
112
- it("warns when managed mode has no teamDomain", async () => {
113
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
114
-
115
- const { default: plugin } = await import("./index.js");
116
- const api = createMockApi({
117
- tunnel: { mode: "managed", tunnelToken: "tok" },
118
- });
119
-
120
- plugin.register(api);
121
-
122
- expect(api.logger.warn).toHaveBeenCalledWith(
123
- expect.stringContaining("no teamDomain configured"),
124
- );
125
- });
126
-
127
- it("registers service and HTTP handler in managed mode", async () => {
128
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
129
-
130
- const { default: plugin } = await import("./index.js");
131
- const api = createMockApi({
132
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
133
- });
134
-
135
- plugin.register(api);
136
-
137
- expect(api.registerService).toHaveBeenCalledTimes(1);
138
- expect(api.registerHttpHandler).toHaveBeenCalledTimes(1);
139
-
140
- const service = api.registerService.mock.calls[0][0];
141
- expect(service.id).toBe("cloudflare-tunnel");
142
- });
143
-
144
- it("service start creates verifier when teamDomain is set", async () => {
145
- const mockVerifier = { verify: vi.fn() };
146
- createCloudflareAccessVerifierMock.mockReturnValue(mockVerifier);
147
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
148
-
149
- const { default: plugin } = await import("./index.js");
150
- const api = createMockApi({
151
- tunnel: {
152
- mode: "managed",
153
- tunnelToken: "tok",
154
- teamDomain: "myteam",
155
- audience: "my-aud",
156
- },
157
- });
158
-
159
- plugin.register(api);
160
-
161
- const service = api.registerService.mock.calls[0][0];
162
- await service.start();
163
-
164
- expect(createCloudflareAccessVerifierMock).toHaveBeenCalledWith({
165
- teamDomain: "myteam",
166
- audience: "my-aud",
167
- });
168
- expect(api.logger.info).toHaveBeenCalledWith(
169
- expect.stringContaining("myteam.cloudflareaccess.com"),
170
- );
171
- });
172
-
173
- it("service start does not create verifier when teamDomain is unset", async () => {
174
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
175
-
176
- const { default: plugin } = await import("./index.js");
177
- const api = createMockApi({
178
- tunnel: { mode: "managed", tunnelToken: "tok" },
179
- });
180
-
181
- plugin.register(api);
182
-
183
- const service = api.registerService.mock.calls[0][0];
184
- await service.start();
185
-
186
- expect(createCloudflareAccessVerifierMock).not.toHaveBeenCalled();
187
- });
188
-
189
- it("service stop calls tunnel stop and clears verifier", async () => {
190
- const tunnelStopFn = vi.fn();
191
- startGatewayCloudflareExposureMock.mockResolvedValue(tunnelStopFn);
192
- createCloudflareAccessVerifierMock.mockReturnValue({ verify: vi.fn() });
193
-
194
- const { default: plugin } = await import("./index.js");
195
- const api = createMockApi({
196
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
197
- });
198
-
199
- plugin.register(api);
200
-
201
- const service = api.registerService.mock.calls[0][0];
202
- await service.start();
203
- await service.stop();
204
-
205
- expect(tunnelStopFn).toHaveBeenCalled();
206
- });
207
-
208
- it("service stop is safe when no tunnel was started", async () => {
209
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
210
-
211
- const { default: plugin } = await import("./index.js");
212
- const api = createMockApi({
213
- tunnel: { mode: "access-only", teamDomain: "myteam" },
214
- });
215
-
216
- plugin.register(api);
217
-
218
- const service = api.registerService.mock.calls[0][0];
219
- await service.start();
220
- // Should not throw
221
- await service.stop();
222
- });
223
-
224
- describe("HTTP handler", () => {
225
- it("strips spoofed identity headers even when no verifier is active", async () => {
226
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
227
-
228
- const { default: plugin } = await import("./index.js");
229
- const api = createMockApi({
230
- tunnel: { mode: "managed", tunnelToken: "tok" },
231
- });
232
-
233
- plugin.register(api);
234
-
235
- const handler = api.registerHttpHandler.mock.calls[0][0];
236
- const req = createMockReq({
237
- "x-openclaw-user-email": "spoofed@evil.com",
238
- "x-openclaw-auth-source": "spoofed",
239
- });
240
- await handler(req, createMockRes());
241
-
242
- expect(req.headers["x-openclaw-user-email"]).toBeUndefined();
243
- expect(req.headers["x-openclaw-auth-source"]).toBeUndefined();
244
- });
245
-
246
- it("strips spoofed identity headers before setting verified ones", async () => {
247
- const mockVerifier = {
248
- verify: vi.fn().mockResolvedValue({ email: "real@example.com" }),
249
- };
250
- createCloudflareAccessVerifierMock.mockReturnValue(mockVerifier);
251
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
252
-
253
- const { default: plugin } = await import("./index.js");
254
- const api = createMockApi({
255
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
256
- });
257
-
258
- plugin.register(api);
259
-
260
- const service = api.registerService.mock.calls[0][0];
261
- await service.start();
262
-
263
- const handler = api.registerHttpHandler.mock.calls[0][0];
264
- const req = createMockReq({
265
- "cf-access-jwt-assertion": "valid-jwt",
266
- "x-openclaw-user-email": "spoofed@evil.com",
267
- "x-openclaw-auth-source": "spoofed",
268
- });
269
- await handler(req, createMockRes());
270
-
271
- expect(req.headers["x-openclaw-user-email"]).toBe("real@example.com");
272
- expect(req.headers["x-openclaw-auth-source"]).toBe("cloudflare-access");
273
- });
274
-
275
- it("returns false when no verifier is active", async () => {
276
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
277
-
278
- const { default: plugin } = await import("./index.js");
279
- const api = createMockApi({
280
- tunnel: { mode: "managed", tunnelToken: "tok" },
281
- });
282
-
283
- plugin.register(api);
284
-
285
- // Service not started yet, so no verifier
286
- const handler = api.registerHttpHandler.mock.calls[0][0];
287
- const req = createMockReq({ "cf-access-jwt-assertion": "some-jwt" });
288
- const result = await handler(req, createMockRes());
289
-
290
- expect(result).toBe(false);
291
- });
292
-
293
- it("returns false when no JWT header is present", async () => {
294
- const mockVerifier = { verify: vi.fn() };
295
- createCloudflareAccessVerifierMock.mockReturnValue(mockVerifier);
296
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
297
-
298
- const { default: plugin } = await import("./index.js");
299
- const api = createMockApi({
300
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
301
- });
302
-
303
- plugin.register(api);
304
-
305
- const service = api.registerService.mock.calls[0][0];
306
- await service.start();
307
-
308
- const handler = api.registerHttpHandler.mock.calls[0][0];
309
- const req = createMockReq({});
310
- const result = await handler(req, createMockRes());
311
-
312
- expect(result).toBe(false);
313
- expect(mockVerifier.verify).not.toHaveBeenCalled();
314
- });
315
-
316
- it("sets identity headers on valid JWT", async () => {
317
- const mockVerifier = {
318
- verify: vi.fn().mockResolvedValue({ email: "alice@example.com" }),
319
- };
320
- createCloudflareAccessVerifierMock.mockReturnValue(mockVerifier);
321
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
322
-
323
- const { default: plugin } = await import("./index.js");
324
- const api = createMockApi({
325
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
326
- });
327
-
328
- plugin.register(api);
329
-
330
- const service = api.registerService.mock.calls[0][0];
331
- await service.start();
332
-
333
- const handler = api.registerHttpHandler.mock.calls[0][0];
334
- const req = createMockReq({ "cf-access-jwt-assertion": "valid-jwt" });
335
- const result = await handler(req, createMockRes());
336
-
337
- expect(result).toBe(false);
338
- expect(mockVerifier.verify).toHaveBeenCalledWith("valid-jwt");
339
- expect(req.headers["x-openclaw-user-email"]).toBe("alice@example.com");
340
- expect(req.headers["x-openclaw-auth-source"]).toBe("cloudflare-access");
341
- });
342
-
343
- it("does not set headers on invalid JWT", async () => {
344
- const mockVerifier = {
345
- verify: vi.fn().mockResolvedValue(null),
346
- };
347
- createCloudflareAccessVerifierMock.mockReturnValue(mockVerifier);
348
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
349
-
350
- const { default: plugin } = await import("./index.js");
351
- const api = createMockApi({
352
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
353
- });
354
-
355
- plugin.register(api);
356
-
357
- const service = api.registerService.mock.calls[0][0];
358
- await service.start();
359
-
360
- const handler = api.registerHttpHandler.mock.calls[0][0];
361
- const req = createMockReq({ "cf-access-jwt-assertion": "bad-jwt" });
362
- const result = await handler(req, createMockRes());
363
-
364
- expect(result).toBe(false);
365
- expect(req.headers["x-openclaw-user-email"]).toBeUndefined();
366
- });
367
-
368
- it("handles array JWT header (takes first value)", async () => {
369
- const mockVerifier = {
370
- verify: vi.fn().mockResolvedValue({ email: "bob@example.com" }),
371
- };
372
- createCloudflareAccessVerifierMock.mockReturnValue(mockVerifier);
373
- startGatewayCloudflareExposureMock.mockResolvedValue(null);
374
-
375
- const { default: plugin } = await import("./index.js");
376
- const api = createMockApi({
377
- tunnel: { mode: "managed", tunnelToken: "tok", teamDomain: "myteam" },
378
- });
379
-
380
- plugin.register(api);
381
-
382
- const service = api.registerService.mock.calls[0][0];
383
- await service.start();
384
-
385
- const handler = api.registerHttpHandler.mock.calls[0][0];
386
- const req = createMockReq({
387
- "cf-access-jwt-assertion": ["first-jwt", "second-jwt"] as unknown as string,
388
- });
389
- const result = await handler(req, createMockRes());
390
-
391
- expect(result).toBe(false);
392
- expect(mockVerifier.verify).toHaveBeenCalledWith("first-jwt");
393
- });
394
- });
395
- });
@@ -1,280 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { describe, expect, it, vi } from "vitest";
3
- import { createCloudflareAccessVerifier } from "./access.js";
4
-
5
- // Generate a test RSA keypair for signing JWTs
6
- const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
7
- modulusLength: 2048,
8
- publicKeyEncoding: { type: "spki", format: "pem" },
9
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
10
- });
11
-
12
- // Export as JWK for the mock JWKS endpoint
13
- const publicJwk = crypto.createPublicKey(publicKey).export({ format: "jwk" }) as {
14
- kty: string;
15
- n: string;
16
- e: string;
17
- };
18
-
19
- const TEST_KID = "test-key-1";
20
- const TEST_TEAM_DOMAIN = "myteam";
21
- const TEST_ISSUER = `https://${TEST_TEAM_DOMAIN}.cloudflareaccess.com`;
22
- const TEST_AUDIENCE = "test-aud-tag";
23
-
24
- function base64UrlEncode(data: Buffer | string): string {
25
- const buf = typeof data === "string" ? Buffer.from(data) : data;
26
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
27
- }
28
-
29
- async function createTestJwt(opts: {
30
- email?: string;
31
- iss?: string;
32
- aud?: string | string[];
33
- exp?: number;
34
- kid?: string;
35
- }): Promise<string> {
36
- const header = {
37
- alg: "RS256",
38
- typ: "JWT",
39
- kid: opts.kid ?? TEST_KID,
40
- };
41
-
42
- const payload = {
43
- email: opts.email ?? "user@example.com",
44
- sub: "user-123",
45
- iss: opts.iss ?? TEST_ISSUER,
46
- aud: opts.aud ?? TEST_AUDIENCE,
47
- exp: opts.exp ?? Math.floor(Date.now() / 1000) + 3600,
48
- iat: Math.floor(Date.now() / 1000),
49
- };
50
-
51
- const headerB64 = base64UrlEncode(JSON.stringify(header));
52
- const payloadB64 = base64UrlEncode(JSON.stringify(payload));
53
- const signingInput = `${headerB64}.${payloadB64}`;
54
-
55
- const sign = crypto.createSign("RSA-SHA256");
56
- sign.update(signingInput);
57
- const signature = sign.sign(privateKey);
58
-
59
- return `${signingInput}.${base64UrlEncode(signature)}`;
60
- }
61
-
62
- function createMockFetch(): typeof globalThis.fetch {
63
- return vi.fn(async (url: string | URL | Request) => {
64
- const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
65
- if (urlStr.includes("/cdn-cgi/access/certs")) {
66
- return new Response(
67
- JSON.stringify({
68
- keys: [
69
- {
70
- kty: publicJwk.kty,
71
- kid: TEST_KID,
72
- alg: "RS256",
73
- use: "sig",
74
- n: publicJwk.n,
75
- e: publicJwk.e,
76
- },
77
- ],
78
- }),
79
- { status: 200 },
80
- );
81
- }
82
- return new Response("Not Found", { status: 404 });
83
- }) as typeof globalThis.fetch;
84
- }
85
-
86
- describe("cloudflare-access verifier", () => {
87
- it("verifies a valid JWT and returns user email", async () => {
88
- const verifier = createCloudflareAccessVerifier({
89
- teamDomain: TEST_TEAM_DOMAIN,
90
- audience: TEST_AUDIENCE,
91
- fetchFn: createMockFetch(),
92
- });
93
-
94
- const token = await createTestJwt({});
95
- const result = await verifier.verify(token);
96
-
97
- expect(result).not.toBeNull();
98
- expect(result!.email).toBe("user@example.com");
99
- });
100
-
101
- it("rejects expired JWT", async () => {
102
- const verifier = createCloudflareAccessVerifier({
103
- teamDomain: TEST_TEAM_DOMAIN,
104
- audience: TEST_AUDIENCE,
105
- fetchFn: createMockFetch(),
106
- });
107
-
108
- const token = await createTestJwt({
109
- exp: Math.floor(Date.now() / 1000) - 3600,
110
- });
111
- const result = await verifier.verify(token);
112
-
113
- expect(result).toBeNull();
114
- });
115
-
116
- it("rejects JWT with wrong issuer", async () => {
117
- const verifier = createCloudflareAccessVerifier({
118
- teamDomain: TEST_TEAM_DOMAIN,
119
- audience: TEST_AUDIENCE,
120
- fetchFn: createMockFetch(),
121
- });
122
-
123
- const token = await createTestJwt({
124
- iss: "https://evil.cloudflareaccess.com",
125
- });
126
- const result = await verifier.verify(token);
127
-
128
- expect(result).toBeNull();
129
- });
130
-
131
- it("rejects JWT with wrong audience when audience is configured", async () => {
132
- const verifier = createCloudflareAccessVerifier({
133
- teamDomain: TEST_TEAM_DOMAIN,
134
- audience: TEST_AUDIENCE,
135
- fetchFn: createMockFetch(),
136
- });
137
-
138
- const token = await createTestJwt({
139
- aud: "wrong-audience",
140
- });
141
- const result = await verifier.verify(token);
142
-
143
- expect(result).toBeNull();
144
- });
145
-
146
- it("accepts any audience when audience is not configured", async () => {
147
- const verifier = createCloudflareAccessVerifier({
148
- teamDomain: TEST_TEAM_DOMAIN,
149
- fetchFn: createMockFetch(),
150
- });
151
-
152
- const token = await createTestJwt({
153
- aud: "any-audience",
154
- });
155
- const result = await verifier.verify(token);
156
-
157
- expect(result).not.toBeNull();
158
- expect(result!.email).toBe("user@example.com");
159
- });
160
-
161
- it("rejects malformed token", async () => {
162
- const verifier = createCloudflareAccessVerifier({
163
- teamDomain: TEST_TEAM_DOMAIN,
164
- fetchFn: createMockFetch(),
165
- });
166
-
167
- const result = await verifier.verify("not.a.valid.jwt");
168
- expect(result).toBeNull();
169
- });
170
-
171
- it("rejects token with unknown kid", async () => {
172
- const verifier = createCloudflareAccessVerifier({
173
- teamDomain: TEST_TEAM_DOMAIN,
174
- fetchFn: createMockFetch(),
175
- });
176
-
177
- const token = await createTestJwt({ kid: "unknown-key-id" });
178
- const result = await verifier.verify(token);
179
-
180
- expect(result).toBeNull();
181
- });
182
-
183
- it("rejects JWT with tampered signature", async () => {
184
- const verifier = createCloudflareAccessVerifier({
185
- teamDomain: TEST_TEAM_DOMAIN,
186
- audience: TEST_AUDIENCE,
187
- fetchFn: createMockFetch(),
188
- });
189
-
190
- const token = await createTestJwt({});
191
- // Tamper with the signature
192
- const parts = token.split(".");
193
- parts[2] = base64UrlEncode(crypto.randomBytes(256));
194
- const tampered = parts.join(".");
195
-
196
- const result = await verifier.verify(tampered);
197
- expect(result).toBeNull();
198
- });
199
-
200
- it("rejects JWT with unsupported algorithm", async () => {
201
- const verifier = createCloudflareAccessVerifier({
202
- teamDomain: TEST_TEAM_DOMAIN,
203
- audience: TEST_AUDIENCE,
204
- fetchFn: createMockFetch(),
205
- });
206
-
207
- // Create a token with alg: "none"
208
- const header = { alg: "none", typ: "JWT", kid: TEST_KID };
209
- const payload = {
210
- email: "user@example.com",
211
- sub: "user-123",
212
- iss: TEST_ISSUER,
213
- aud: TEST_AUDIENCE,
214
- exp: Math.floor(Date.now() / 1000) + 3600,
215
- iat: Math.floor(Date.now() / 1000),
216
- };
217
- const headerB64 = base64UrlEncode(JSON.stringify(header));
218
- const payloadB64 = base64UrlEncode(JSON.stringify(payload));
219
- const token = `${headerB64}.${payloadB64}.${base64UrlEncode("fake")}`;
220
-
221
- const result = await verifier.verify(token);
222
- expect(result).toBeNull();
223
- });
224
-
225
- it("rejects JWT with tampered payload but original signature", async () => {
226
- const verifier = createCloudflareAccessVerifier({
227
- teamDomain: TEST_TEAM_DOMAIN,
228
- audience: TEST_AUDIENCE,
229
- fetchFn: createMockFetch(),
230
- });
231
-
232
- const token = await createTestJwt({});
233
- const parts = token.split(".");
234
-
235
- // Tamper with the payload (change email) but keep original signature
236
- const tamperedPayload = {
237
- email: "attacker@evil.com",
238
- sub: "user-123",
239
- iss: TEST_ISSUER,
240
- aud: TEST_AUDIENCE,
241
- exp: Math.floor(Date.now() / 1000) + 3600,
242
- iat: Math.floor(Date.now() / 1000),
243
- };
244
- parts[1] = base64UrlEncode(JSON.stringify(tamperedPayload));
245
- const tampered = parts.join(".");
246
-
247
- const result = await verifier.verify(tampered);
248
- expect(result).toBeNull();
249
- });
250
-
251
- it("rejects JWT with no email claim", async () => {
252
- const verifier = createCloudflareAccessVerifier({
253
- teamDomain: TEST_TEAM_DOMAIN,
254
- fetchFn: createMockFetch(),
255
- });
256
-
257
- // Create a token with no email
258
- const header = {
259
- alg: "RS256",
260
- typ: "JWT",
261
- kid: TEST_KID,
262
- };
263
- const payload = {
264
- sub: "service-token",
265
- iss: TEST_ISSUER,
266
- exp: Math.floor(Date.now() / 1000) + 3600,
267
- iat: Math.floor(Date.now() / 1000),
268
- };
269
- const headerB64 = base64UrlEncode(JSON.stringify(header));
270
- const payloadB64 = base64UrlEncode(JSON.stringify(payload));
271
- const signingInput = `${headerB64}.${payloadB64}`;
272
- const sign = crypto.createSign("RSA-SHA256");
273
- sign.update(signingInput);
274
- const signature = sign.sign(privateKey);
275
- const token = `${signingInput}.${base64UrlEncode(signature)}`;
276
-
277
- const result = await verifier.verify(token);
278
- expect(result).toBeNull();
279
- });
280
- });