cloudflare-access 1.0.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 +21 -0
- package/README.md +452 -0
- package/dist/adapters/effect/index.d.mts +167 -0
- package/dist/adapters/effect/index.d.ts +167 -0
- package/dist/adapters/effect/index.js +221 -0
- package/dist/adapters/effect/index.js.map +1 -0
- package/dist/adapters/effect/index.mjs +221 -0
- package/dist/adapters/effect/index.mjs.map +1 -0
- package/dist/adapters/express/index.d.mts +74 -0
- package/dist/adapters/express/index.d.ts +74 -0
- package/dist/adapters/express/index.js +129 -0
- package/dist/adapters/express/index.js.map +1 -0
- package/dist/adapters/express/index.mjs +129 -0
- package/dist/adapters/express/index.mjs.map +1 -0
- package/dist/adapters/fastify/index.d.mts +111 -0
- package/dist/adapters/fastify/index.d.ts +111 -0
- package/dist/adapters/fastify/index.js +140 -0
- package/dist/adapters/fastify/index.js.map +1 -0
- package/dist/adapters/fastify/index.mjs +140 -0
- package/dist/adapters/fastify/index.mjs.map +1 -0
- package/dist/adapters/hono/index.d.mts +19 -0
- package/dist/adapters/hono/index.d.ts +19 -0
- package/dist/adapters/hono/index.js +45 -0
- package/dist/adapters/hono/index.js.map +1 -0
- package/dist/adapters/hono/index.mjs +45 -0
- package/dist/adapters/hono/index.mjs.map +1 -0
- package/dist/adapters/nestjs/index.d.mts +123 -0
- package/dist/adapters/nestjs/index.d.ts +123 -0
- package/dist/adapters/nestjs/index.js +117 -0
- package/dist/adapters/nestjs/index.js.map +1 -0
- package/dist/adapters/nestjs/index.mjs +117 -0
- package/dist/adapters/nestjs/index.mjs.map +1 -0
- package/dist/chunk-DM2KGIQX.mjs +320 -0
- package/dist/chunk-DM2KGIQX.mjs.map +1 -0
- package/dist/chunk-LQWCGHLJ.mjs +108 -0
- package/dist/chunk-LQWCGHLJ.mjs.map +1 -0
- package/dist/chunk-PMFPT3SI.js +108 -0
- package/dist/chunk-PMFPT3SI.js.map +1 -0
- package/dist/chunk-WUJPWM4T.js +320 -0
- package/dist/chunk-WUJPWM4T.js.map +1 -0
- package/dist/config-D4O7DXNT.d.mts +12 -0
- package/dist/config-ottUdc-K.d.ts +12 -0
- package/dist/core/index.d.mts +24 -0
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.js +41 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +41 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +41 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jwks-ChdyyS_L.d.mts +173 -0
- package/dist/jwks-ChdyyS_L.d.ts +173 -0
- package/dist/middleware-BDl6jUCu.d.mts +83 -0
- package/dist/middleware-CgFsjM20.d.ts +83 -0
- package/examples/basic.ts +52 -0
- package/examples/cloudflare-workers.ts +84 -0
- package/examples/custom-handlers.ts +85 -0
- package/examples/effect/http-server.ts +205 -0
- package/examples/email-allowlist.ts +50 -0
- package/examples/express/basic.ts +26 -0
- package/examples/fastify/basic.ts +24 -0
- package/examples/hono/basic.ts +26 -0
- package/examples/hono-router.ts +74 -0
- package/examples/nestjs/basic.ts +39 -0
- package/examples/skip-dev-mode.ts +89 -0
- package/package.json +178 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# cloudflare-access
|
|
2
|
+
|
|
3
|
+
Multi-framework authentication for Cloudflare Access with JWT validation.
|
|
4
|
+
|
|
5
|
+
Supports: **Hono**, **Express**, **Fastify**, **NestJS**, and **Effect-TS**.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# npm
|
|
11
|
+
npm install cloudflare-access
|
|
12
|
+
|
|
13
|
+
# yarn
|
|
14
|
+
yarn add cloudflare-access
|
|
15
|
+
|
|
16
|
+
# bun
|
|
17
|
+
bun add cloudflare-access
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Framework-Specific Installation
|
|
21
|
+
|
|
22
|
+
### Hono
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun add cloudflare-access hono
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Express
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun add cloudflare-access express
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Fastify
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun add cloudflare-access fastify
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### NestJS
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bun add cloudflare-access @nestjs/common @nestjs/core
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Effect-TS
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bun add cloudflare-access effect
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
### Hono
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Hono } from "hono";
|
|
58
|
+
import { createCloudflareAccessAuth } from "cloudflare-access/hono";
|
|
59
|
+
|
|
60
|
+
const app = new Hono();
|
|
61
|
+
|
|
62
|
+
// Using static configuration (works with any deployment target)
|
|
63
|
+
app.use(
|
|
64
|
+
createCloudflareAccessAuth({
|
|
65
|
+
accessConfig: {
|
|
66
|
+
teamDomain: process.env.CF_ACCESS_TEAM_DOMAIN!,
|
|
67
|
+
audTag: process.env.CF_ACCESS_AUD!,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Or with Cloudflare Workers bindings
|
|
73
|
+
// import { getCloudflareAccessConfigFromBindings } from "cloudflare-access/hono";
|
|
74
|
+
// app.use(createCloudflareAccessAuth({
|
|
75
|
+
// accessConfig: getCloudflareAccessConfigFromBindings,
|
|
76
|
+
// }));
|
|
77
|
+
|
|
78
|
+
app.get("/protected", (c) => {
|
|
79
|
+
const user = c.get("user");
|
|
80
|
+
return c.json({ email: user?.email });
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Express
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import express from "express";
|
|
88
|
+
import { cloudflareAccessAuth } from "cloudflare-access/express";
|
|
89
|
+
|
|
90
|
+
const app = express();
|
|
91
|
+
|
|
92
|
+
app.use(
|
|
93
|
+
cloudflareAccessAuth({
|
|
94
|
+
accessConfig: {
|
|
95
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
96
|
+
audTag: "your-audience-tag",
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
app.get("/protected", (req, res) => {
|
|
102
|
+
res.json({ email: req.user?.email });
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Fastify
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import fastify from "fastify";
|
|
110
|
+
import { cloudflareAccessPlugin } from "cloudflare-access/fastify";
|
|
111
|
+
|
|
112
|
+
const app = fastify();
|
|
113
|
+
|
|
114
|
+
app.register(cloudflareAccessPlugin, {
|
|
115
|
+
accessConfig: {
|
|
116
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
117
|
+
audTag: "your-audience-tag",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
app.get("/protected", async (request, reply) => {
|
|
122
|
+
return { email: request.user?.email };
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### NestJS
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { Controller, Get, Req, Module } from "@nestjs/common";
|
|
130
|
+
import { CloudflareAccessGuard, Public } from "cloudflare-access/nestjs";
|
|
131
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
132
|
+
import type { Request } from "express";
|
|
133
|
+
|
|
134
|
+
@Controller("api")
|
|
135
|
+
export class ApiController {
|
|
136
|
+
@Get("protected")
|
|
137
|
+
getProtected(@Req() req: Request) {
|
|
138
|
+
return { email: req.user?.email };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Public()
|
|
142
|
+
@Get("public")
|
|
143
|
+
getPublic() {
|
|
144
|
+
return { message: "This is public" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Module({
|
|
149
|
+
controllers: [ApiController],
|
|
150
|
+
providers: [
|
|
151
|
+
{
|
|
152
|
+
provide: APP_GUARD,
|
|
153
|
+
useFactory: () =>
|
|
154
|
+
new CloudflareAccessGuard({
|
|
155
|
+
accessConfig: {
|
|
156
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
157
|
+
audTag: "your-audience-tag",
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
})
|
|
163
|
+
export class AppModule {}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Effect-TS
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { Effect, Either } from "effect";
|
|
170
|
+
import { HttpServerRequest } from "@effect/platform";
|
|
171
|
+
import { authenticateRequest } from "cloudflare-access/effect";
|
|
172
|
+
|
|
173
|
+
const request: HttpServerRequest.HttpServerRequest = {
|
|
174
|
+
url: "https://example.com/api/protected",
|
|
175
|
+
headers: {
|
|
176
|
+
"cf-access-jwt-assertion": "jwt-token-here",
|
|
177
|
+
},
|
|
178
|
+
method: "GET",
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const program = Effect.gen(function* () {
|
|
182
|
+
const result = yield* Effect.either(
|
|
183
|
+
authenticateRequest(request, {
|
|
184
|
+
accessConfig: {
|
|
185
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
186
|
+
audTag: "your-audience-tag",
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return Either.match(result, {
|
|
192
|
+
onLeft: (error) => {
|
|
193
|
+
console.error(`Auth failed: ${error.message}`);
|
|
194
|
+
return null;
|
|
195
|
+
},
|
|
196
|
+
onRight: (user) => {
|
|
197
|
+
console.log(`Authenticated user: ${user.email}`);
|
|
198
|
+
return user;
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
Effect.runPromise(program);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Configuration
|
|
207
|
+
|
|
208
|
+
### Environment Variables
|
|
209
|
+
|
|
210
|
+
Configure your Cloudflare Access credentials via environment variables:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# .env
|
|
214
|
+
CF_ACCESS_TEAM_DOMAIN=https://yourteam.cloudflareaccess.com
|
|
215
|
+
CF_ACCESS_AUD=your-audience-tag
|
|
216
|
+
ENVIRONMENT=dev
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Or set them directly in your environment:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
export CF_ACCESS_TEAM_DOMAIN="https://yourteam.cloudflareaccess.com"
|
|
223
|
+
export CF_ACCESS_AUD="your-audience-tag"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
For **Cloudflare Workers**, use `wrangler.toml`:
|
|
227
|
+
|
|
228
|
+
```toml
|
|
229
|
+
[vars]
|
|
230
|
+
CF_ACCESS_TEAM_DOMAIN = "https://yourteam.cloudflareaccess.com"
|
|
231
|
+
CF_ACCESS_AUD = "your-audience-tag"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Or with Wrangler secrets:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
wrangler secret put CF_ACCESS_AUD
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Common Options
|
|
241
|
+
|
|
242
|
+
All framework adapters support these options:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
interface CloudflareAccessAuthOptions {
|
|
246
|
+
/** Cloudflare Access configuration */
|
|
247
|
+
accessConfig: CloudflareAccessConfig;
|
|
248
|
+
|
|
249
|
+
/** Optional email allowlist */
|
|
250
|
+
allowedEmails?: string[];
|
|
251
|
+
|
|
252
|
+
/** Custom unauthorized handler */
|
|
253
|
+
onUnauthorized?: (reason: string) => Response | void | Promise<void>;
|
|
254
|
+
|
|
255
|
+
/** Custom forbidden handler */
|
|
256
|
+
onForbidden?: (email: string) => Response | void | Promise<void>;
|
|
257
|
+
|
|
258
|
+
/** Paths to exclude from authentication */
|
|
259
|
+
excludePaths?: string[];
|
|
260
|
+
|
|
261
|
+
/** Skip JWT validation in development (localhost requests) */
|
|
262
|
+
skipInDev?: boolean;
|
|
263
|
+
|
|
264
|
+
/** Environment indicator */
|
|
265
|
+
environment?: string;
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## User Information
|
|
270
|
+
|
|
271
|
+
After successful authentication, user information is available:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
interface CloudflareAccessUser {
|
|
275
|
+
email: string;
|
|
276
|
+
userId?: string;
|
|
277
|
+
country?: string;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Rich Error Handling
|
|
282
|
+
|
|
283
|
+
The library provides rich, actionable errors with detailed context:
|
|
284
|
+
|
|
285
|
+
### Error Classes
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import {
|
|
289
|
+
CloudflareAccessError,
|
|
290
|
+
AuthRequiredError,
|
|
291
|
+
InvalidTokenError,
|
|
292
|
+
AccessDeniedError,
|
|
293
|
+
ConfigurationError,
|
|
294
|
+
// Type guards
|
|
295
|
+
isAuthRequiredError,
|
|
296
|
+
isInvalidTokenError,
|
|
297
|
+
isAccessDeniedError,
|
|
298
|
+
} from "cloudflare-access/core";
|
|
299
|
+
|
|
300
|
+
// All errors extend CloudflareAccessError
|
|
301
|
+
try {
|
|
302
|
+
await validateCloudflareAccessToken(token, options, url);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (isAuthRequiredError(error)) {
|
|
305
|
+
console.log("Auth required:", error.message);
|
|
306
|
+
console.log("Fix:", error.context?.fix);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isInvalidTokenError(error)) {
|
|
310
|
+
console.log("Invalid token:", error.reason);
|
|
311
|
+
console.log("Token info:", error.context?.tokenInfo);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (isAccessDeniedError(error)) {
|
|
315
|
+
console.log("Access denied for:", error.email);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// All errors are serializable
|
|
319
|
+
console.log("Full error:", error.toJSON());
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Error Codes
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { CloudflareAccessErrorCode } from "cloudflare-access/core";
|
|
327
|
+
|
|
328
|
+
// Available error codes:
|
|
329
|
+
// - AUTH_REQUIRED: Missing or invalid authentication token
|
|
330
|
+
// - INVALID_TOKEN: Token validation failed (expired, wrong signature, etc.)
|
|
331
|
+
// - ACCESS_DENIED: User email not in allowlist
|
|
332
|
+
// - INVALID_TEAM_DOMAIN: Invalid team domain configuration
|
|
333
|
+
// - MISSING_AUDIENCE_TAG: Missing audience tag
|
|
334
|
+
// - MISSING_CONFIG: Missing environment configuration
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Effect-TS Error Handling
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { Effect, Either } from "effect";
|
|
341
|
+
import {
|
|
342
|
+
authenticateRequest,
|
|
343
|
+
AuthRequiredError,
|
|
344
|
+
InvalidTokenError,
|
|
345
|
+
AccessDeniedError,
|
|
346
|
+
} from "cloudflare-access/effect";
|
|
347
|
+
import { HttpServerRequest } from "@effect/platform";
|
|
348
|
+
|
|
349
|
+
const request: HttpServerRequest.HttpServerRequest = {
|
|
350
|
+
url: "https://example.com/api/protected",
|
|
351
|
+
headers: { "cf-access-jwt-assertion": "token" },
|
|
352
|
+
method: "GET",
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const program = Effect.gen(function* () {
|
|
356
|
+
const result = yield* Effect.either(
|
|
357
|
+
authenticateRequest(request, {
|
|
358
|
+
accessConfig: {
|
|
359
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
360
|
+
audTag: "your-audience-tag",
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
return Either.match(result, {
|
|
366
|
+
onLeft: (error) => {
|
|
367
|
+
if (error instanceof AuthRequiredError) {
|
|
368
|
+
console.log("Auth required:", error.message);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
if (error instanceof AccessDeniedError) {
|
|
372
|
+
console.log("Access denied for:", error.email);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
},
|
|
377
|
+
onRight: (user) => user,
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Using the Core Module Directly
|
|
383
|
+
|
|
384
|
+
If you need lower-level access to the authentication logic:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import {
|
|
388
|
+
validateCloudflareAccessToken,
|
|
389
|
+
getCloudflareAccessConfigFromEnv,
|
|
390
|
+
type CloudflareAccessConfig,
|
|
391
|
+
} from "cloudflare-access/core";
|
|
392
|
+
|
|
393
|
+
const result = await validateCloudflareAccessToken(
|
|
394
|
+
token,
|
|
395
|
+
{ accessConfig: { teamDomain, audTag } },
|
|
396
|
+
requestUrl,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (result.success) {
|
|
400
|
+
console.log("User:", result.user);
|
|
401
|
+
} else {
|
|
402
|
+
console.log("Auth failed:", result.error);
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Package Imports
|
|
407
|
+
|
|
408
|
+
The package uses subpath exports for each framework:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Core authentication logic
|
|
412
|
+
import { validateCloudflareAccessToken } from "cloudflare-access/core";
|
|
413
|
+
|
|
414
|
+
// Framework adapters
|
|
415
|
+
import { createCloudflareAccessAuth } from "cloudflare-access/hono";
|
|
416
|
+
import { cloudflareAccessAuth } from "cloudflare-access/express";
|
|
417
|
+
import { cloudflareAccessPlugin } from "cloudflare-access/fastify";
|
|
418
|
+
import { CloudflareAccessGuard } from "cloudflare-access/nestjs";
|
|
419
|
+
import { authenticateRequest } from "cloudflare-access/effect";
|
|
420
|
+
|
|
421
|
+
// Hono exports (from root)
|
|
422
|
+
import {
|
|
423
|
+
createCloudflareAccessAuth,
|
|
424
|
+
getCloudflareAccessConfigFromBindings,
|
|
425
|
+
} from "cloudflare-access";
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Examples
|
|
429
|
+
|
|
430
|
+
See the `/examples` directory for detailed examples:
|
|
431
|
+
|
|
432
|
+
- `examples/hono/` - Hono framework examples
|
|
433
|
+
- `examples/express/` - Express framework examples
|
|
434
|
+
- `examples/fastify/` - Fastify framework examples
|
|
435
|
+
- `examples/nestjs/` - NestJS framework examples
|
|
436
|
+
- `examples/effect/` - Effect-TS framework examples
|
|
437
|
+
|
|
438
|
+
## Testing
|
|
439
|
+
|
|
440
|
+
The library includes comprehensive test coverage for all adapters:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
# Run all tests
|
|
444
|
+
bun test
|
|
445
|
+
|
|
446
|
+
# Run with coverage
|
|
447
|
+
bun test --coverage
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## License
|
|
451
|
+
|
|
452
|
+
MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { C as CloudflareAccessConfig, g as CloudflareAccessUser, c as CloudflareAccessError } from '../../jwks-ChdyyS_L.mjs';
|
|
2
|
+
export { A as AccessDeniedError, a as AuthRequiredError, b as AuthResult, d as CloudflareAccessErrorCode, e as CloudflareAccessMiddlewareEnv, f as CloudflareAccessPayload, h as ConfigurationError, I as InvalidTokenError, _ as __clearJwksCache, i as isAccessDeniedError, j as isAuthRequiredError, k as isCloudflareAccessError, l as isConfigurationError, m as isInvalidTokenError, t as toAuthError } from '../../jwks-ChdyyS_L.mjs';
|
|
3
|
+
export { g as getCloudflareAccessConfigFromEnv } from '../../config-D4O7DXNT.mjs';
|
|
4
|
+
import { Schema, Context, Redacted, Effect, Layer, Option } from 'effect';
|
|
5
|
+
import * as _effect_platform_HttpRouter from '@effect/platform/HttpRouter';
|
|
6
|
+
import { HttpApiMiddleware, HttpApiSecurity, HttpServerRequest } from '@effect/platform';
|
|
7
|
+
import * as effect_Either from 'effect/Either';
|
|
8
|
+
import 'jose';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for creating Cloudflare Access middleware
|
|
12
|
+
*/
|
|
13
|
+
interface CloudflareAccessMiddlewareOptions {
|
|
14
|
+
/** Cloudflare Access configuration */
|
|
15
|
+
accessConfig: CloudflareAccessConfig;
|
|
16
|
+
/** Optional email allowlist */
|
|
17
|
+
allowedEmails?: string[];
|
|
18
|
+
/** Skip validation in development */
|
|
19
|
+
skipInDev?: boolean;
|
|
20
|
+
/** Environment identifier */
|
|
21
|
+
environment?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare const Unauthorized_base: Schema.TaggedErrorClass<Unauthorized, "Unauthorized", {
|
|
25
|
+
readonly _tag: Schema.tag<"Unauthorized">;
|
|
26
|
+
} & {
|
|
27
|
+
message: typeof Schema.String;
|
|
28
|
+
code: typeof Schema.String;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Unauthorized error schema for Effect Platform
|
|
32
|
+
*/
|
|
33
|
+
declare class Unauthorized extends Unauthorized_base {
|
|
34
|
+
}
|
|
35
|
+
declare const Forbidden_base: Schema.TaggedErrorClass<Forbidden, "Forbidden", {
|
|
36
|
+
readonly _tag: Schema.tag<"Forbidden">;
|
|
37
|
+
} & {
|
|
38
|
+
message: typeof Schema.String;
|
|
39
|
+
email: typeof Schema.String;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Forbidden error schema for Effect Platform
|
|
43
|
+
*/
|
|
44
|
+
declare class Forbidden extends Forbidden_base {
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare const CurrentUser_base: Context.TagClass<CurrentUser, "cloudflare-access/CurrentUser", CloudflareAccessUser>;
|
|
48
|
+
/**
|
|
49
|
+
* Context Tag for the authenticated user.
|
|
50
|
+
* Use this to access the current user in your handlers.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* import { Effect } from "effect";
|
|
55
|
+
* import { CurrentUser } from "cloudflare-access/effect";
|
|
56
|
+
*
|
|
57
|
+
* const handler = Effect.gen(function* () {
|
|
58
|
+
* const user = yield* CurrentUser;
|
|
59
|
+
* console.log(`Hello ${user.email}`);
|
|
60
|
+
* return user;
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare class CurrentUser extends CurrentUser_base {
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare const CloudflareAccessAuth_base: HttpApiMiddleware.TagClass.BaseSecurity<CloudflareAccessAuth, "CloudflareAccessAuth", {
|
|
68
|
+
readonly provides: typeof CurrentUser;
|
|
69
|
+
readonly failure: Schema.Union<[typeof Unauthorized, typeof Forbidden]>;
|
|
70
|
+
readonly security: {
|
|
71
|
+
readonly bearer: HttpApiSecurity.Bearer;
|
|
72
|
+
};
|
|
73
|
+
}, {
|
|
74
|
+
readonly bearer: (_: Redacted.Redacted<string>) => Effect.Effect<CloudflareAccessUser, Unauthorized | Forbidden, _effect_platform_HttpRouter.HttpRouter.Provided>;
|
|
75
|
+
}, {
|
|
76
|
+
readonly bearer: HttpApiSecurity.Bearer;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Cloudflare Access authentication middleware class.
|
|
80
|
+
*
|
|
81
|
+
* Use this class directly in your API middleware definitions.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { HttpApi, HttpApiGroup, HttpApiEndpoint } from "@effect/platform";
|
|
86
|
+
* import { Schema, Effect, Layer } from "effect";
|
|
87
|
+
* import {
|
|
88
|
+
* CloudflareAccessAuth,
|
|
89
|
+
* CurrentUser,
|
|
90
|
+
* makeCloudflareAccessLive,
|
|
91
|
+
* Unauthorized,
|
|
92
|
+
* Forbidden
|
|
93
|
+
* } from "cloudflare-access/effect";
|
|
94
|
+
*
|
|
95
|
+
* // Configure the middleware
|
|
96
|
+
* const CloudflareAccessLive = makeCloudflareAccessLive({
|
|
97
|
+
* accessConfig: {
|
|
98
|
+
* teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
99
|
+
* audTag: "your-audience-tag",
|
|
100
|
+
* },
|
|
101
|
+
* allowedEmails: ["admin@example.com"],
|
|
102
|
+
* skipInDev: true,
|
|
103
|
+
* environment: "dev",
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* // Define API with protected endpoints
|
|
107
|
+
* const User = Schema.Struct({ email: Schema.String });
|
|
108
|
+
*
|
|
109
|
+
* const api = HttpApi.make("api").add(
|
|
110
|
+
* HttpApiGroup.make("users").add(
|
|
111
|
+
* HttpApiEndpoint.get("profile", "/profile")
|
|
112
|
+
* .addSuccess(User)
|
|
113
|
+
* .middleware(CloudflareAccessAuth)
|
|
114
|
+
* )
|
|
115
|
+
* );
|
|
116
|
+
*
|
|
117
|
+
* // Implement handlers with access to CurrentUser
|
|
118
|
+
* const UsersLive = HttpApiBuilder.group(api, "users", (handlers) =>
|
|
119
|
+
* Effect.gen(function* () {
|
|
120
|
+
* const user = yield* CurrentUser;
|
|
121
|
+
* return handlers.handle("profile", () =>
|
|
122
|
+
* Effect.succeed({ email: user.email })
|
|
123
|
+
* );
|
|
124
|
+
* })
|
|
125
|
+
* ).pipe(Layer.provide(CloudflareAccessLive));
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare class CloudflareAccessAuth extends CloudflareAccessAuth_base {
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create a Layer that implements Cloudflare Access authentication.
|
|
132
|
+
*
|
|
133
|
+
* This layer validates the Cloudflare Access JWT token from the request
|
|
134
|
+
* and provides the authenticated user via the CurrentUser context.
|
|
135
|
+
*
|
|
136
|
+
* @param options Configuration for Cloudflare Access
|
|
137
|
+
* @returns Layer that provides authentication
|
|
138
|
+
*/
|
|
139
|
+
declare const makeCloudflareAccessLive: (options: CloudflareAccessMiddlewareOptions) => Layer.Layer<CloudflareAccessAuth, never, never>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract Cloudflare Access token from request headers.
|
|
143
|
+
* Looks for CF-Access-JWT-Assertion header.
|
|
144
|
+
*
|
|
145
|
+
* @param request HTTP server request
|
|
146
|
+
* @returns Option with token if present
|
|
147
|
+
*/
|
|
148
|
+
declare const extractToken: (request: HttpServerRequest.HttpServerRequest) => Option.Option<string>;
|
|
149
|
+
/**
|
|
150
|
+
* Authenticate using the CF-Access-JWT-Assertion header directly.
|
|
151
|
+
* This bypasses the HttpApiSecurity bearer token pattern.
|
|
152
|
+
*
|
|
153
|
+
* @param request HTTP server request
|
|
154
|
+
* @param options Authentication options
|
|
155
|
+
* @returns Effect with authenticated user or error
|
|
156
|
+
*/
|
|
157
|
+
declare const authenticateRequest: (request: HttpServerRequest.HttpServerRequest, options: CloudflareAccessMiddlewareOptions) => Effect.Effect<CloudflareAccessUser, CloudflareAccessError, never>;
|
|
158
|
+
/**
|
|
159
|
+
* Get user as Option (returns None on auth failure)
|
|
160
|
+
*/
|
|
161
|
+
declare const getUser: (request: HttpServerRequest.HttpServerRequest, options: CloudflareAccessMiddlewareOptions) => Effect.Effect<Option.Option<CloudflareAccessUser>, never, never>;
|
|
162
|
+
/**
|
|
163
|
+
* Authenticate and return Either (for explicit error handling)
|
|
164
|
+
*/
|
|
165
|
+
declare const authenticateEither: (request: HttpServerRequest.HttpServerRequest, options: CloudflareAccessMiddlewareOptions) => Effect.Effect<effect_Either.Either<CloudflareAccessUser, CloudflareAccessError>, never, never>;
|
|
166
|
+
|
|
167
|
+
export { CloudflareAccessAuth, CloudflareAccessConfig, CloudflareAccessError, type CloudflareAccessMiddlewareOptions, CloudflareAccessUser, CurrentUser, Forbidden, Unauthorized, authenticateEither, authenticateRequest, extractToken, getUser, makeCloudflareAccessLive };
|