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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Error Handlers Example
|
|
3
|
+
*
|
|
4
|
+
* Customize the response when authentication fails or access is denied.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import {
|
|
9
|
+
createCloudflareAccessAuth,
|
|
10
|
+
getCloudflareAccessConfigFromBindings,
|
|
11
|
+
type CloudflareAccessUser,
|
|
12
|
+
} from "cloudflare-access/hono";
|
|
13
|
+
|
|
14
|
+
interface Bindings {
|
|
15
|
+
CF_ACCESS_TEAM_DOMAIN: string;
|
|
16
|
+
CF_ACCESS_AUD: string;
|
|
17
|
+
ENVIRONMENT: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Variables {
|
|
21
|
+
user?: CloudflareAccessUser;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
25
|
+
|
|
26
|
+
app.use(
|
|
27
|
+
createCloudflareAccessAuth({
|
|
28
|
+
accessConfig: getCloudflareAccessConfigFromBindings,
|
|
29
|
+
allowedEmails: ["admin@example.com"],
|
|
30
|
+
|
|
31
|
+
// Custom unauthorized response
|
|
32
|
+
onUnauthorized: (c, reason) => {
|
|
33
|
+
return c.json(
|
|
34
|
+
{
|
|
35
|
+
error: "Authentication Required",
|
|
36
|
+
message: "You must sign in to access this resource",
|
|
37
|
+
reason,
|
|
38
|
+
documentation: "https://docs.example.com/auth",
|
|
39
|
+
support: "support@example.com",
|
|
40
|
+
},
|
|
41
|
+
401,
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Custom forbidden response
|
|
46
|
+
onForbidden: (c, email) => {
|
|
47
|
+
return c.json(
|
|
48
|
+
{
|
|
49
|
+
error: "Access Denied",
|
|
50
|
+
message: `Sorry ${email}, you don't have permission to access this resource`,
|
|
51
|
+
requiredRole: "admin",
|
|
52
|
+
currentUser: email,
|
|
53
|
+
upgradeRequestUrl: "https://example.com/request-access",
|
|
54
|
+
},
|
|
55
|
+
403,
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Skip auth for these paths
|
|
60
|
+
excludePaths: ["/api/public", "/health", "/docs"],
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Public routes (no auth required)
|
|
65
|
+
app.get("/api/public", (c) => {
|
|
66
|
+
return c.json({
|
|
67
|
+
message: "This is public data",
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.get("/health", (c) => {
|
|
73
|
+
return c.json({ status: "healthy" });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Protected routes
|
|
77
|
+
app.get("/api/private", (c) => {
|
|
78
|
+
const user = c.get("user");
|
|
79
|
+
return c.json({
|
|
80
|
+
message: "Private data",
|
|
81
|
+
accessedBy: user?.email,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export default app;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
|
|
2
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
|
|
3
|
+
import { Effect, Layer, Schema } from "effect";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import {
|
|
6
|
+
CloudflareAccessAuth,
|
|
7
|
+
makeCloudflareAccessLive,
|
|
8
|
+
CurrentUser,
|
|
9
|
+
Unauthorized,
|
|
10
|
+
Forbidden,
|
|
11
|
+
} from "cloudflare-access/effect";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// 1. SCHEMAS
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Schema for authenticated user
|
|
19
|
+
*/
|
|
20
|
+
const User = Schema.Struct({
|
|
21
|
+
email: Schema.String,
|
|
22
|
+
userId: Schema.optional(Schema.String),
|
|
23
|
+
country: Schema.optional(Schema.String),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Schema for user profile response
|
|
28
|
+
*/
|
|
29
|
+
const UserProfile = Schema.Struct({
|
|
30
|
+
email: Schema.String,
|
|
31
|
+
userId: Schema.optional(Schema.String),
|
|
32
|
+
country: Schema.optional(Schema.String),
|
|
33
|
+
preferences: Schema.Struct({
|
|
34
|
+
theme: Schema.String,
|
|
35
|
+
notifications: Schema.Boolean,
|
|
36
|
+
}),
|
|
37
|
+
lastLogin: Schema.String,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Schema for admin dashboard response
|
|
42
|
+
*/
|
|
43
|
+
const AdminStats = Schema.Struct({
|
|
44
|
+
totalUsers: Schema.Number,
|
|
45
|
+
activeSessions: Schema.Number,
|
|
46
|
+
apiRequests: Schema.Number,
|
|
47
|
+
recentActivity: Schema.Array(Schema.String),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// 2. API DEFINITION
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Users API Group - Protected endpoints
|
|
56
|
+
*/
|
|
57
|
+
const UsersGroup = HttpApiGroup.make("users")
|
|
58
|
+
.add(
|
|
59
|
+
HttpApiEndpoint.get("getProfile", "/users/me")
|
|
60
|
+
.addSuccess(UserProfile)
|
|
61
|
+
.addError(Unauthorized, { status: 401 })
|
|
62
|
+
.addError(Forbidden, { status: 403 }),
|
|
63
|
+
)
|
|
64
|
+
.middleware(CloudflareAccessAuth);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Admin API Group - Admin-only endpoints
|
|
68
|
+
*/
|
|
69
|
+
const AdminGroup = HttpApiGroup.make("admin")
|
|
70
|
+
.add(
|
|
71
|
+
HttpApiEndpoint.get("getStats", "/admin/stats")
|
|
72
|
+
.addSuccess(AdminStats)
|
|
73
|
+
.addError(Unauthorized, { status: 401 })
|
|
74
|
+
.addError(Forbidden, { status: 403 }),
|
|
75
|
+
)
|
|
76
|
+
.add(
|
|
77
|
+
HttpApiEndpoint.get("getUsers", "/admin/users")
|
|
78
|
+
.addSuccess(Schema.Array(User))
|
|
79
|
+
.addError(Unauthorized, { status: 401 })
|
|
80
|
+
.addError(Forbidden, { status: 403 }),
|
|
81
|
+
)
|
|
82
|
+
.middleware(CloudflareAccessAuth);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Public API Group - No authentication required
|
|
86
|
+
*/
|
|
87
|
+
const PublicGroup = HttpApiGroup.make("public").add(
|
|
88
|
+
HttpApiEndpoint.get("health", "/health").addSuccess(Schema.Struct({ status: Schema.String })),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Main API definition
|
|
93
|
+
*/
|
|
94
|
+
const Api = HttpApi.make("CloudflareAccessApi").add(PublicGroup).add(UsersGroup).add(AdminGroup);
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// 3. IMPLEMENTATION
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Authentication layer - provides the middleware implementation
|
|
102
|
+
*/
|
|
103
|
+
const CloudflareAccessLive = makeCloudflareAccessLive({
|
|
104
|
+
accessConfig: {
|
|
105
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
106
|
+
audTag: "your-audience-tag",
|
|
107
|
+
},
|
|
108
|
+
allowedEmails: ["admin@example.com", "user@example.com"],
|
|
109
|
+
skipInDev: true,
|
|
110
|
+
environment: "dev",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Implement the Users group
|
|
115
|
+
*/
|
|
116
|
+
const UsersLive = HttpApiBuilder.group(Api, "users", (handlers) =>
|
|
117
|
+
handlers.handle("getProfile", () =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const user = yield* CurrentUser;
|
|
120
|
+
return {
|
|
121
|
+
email: user.email,
|
|
122
|
+
userId: user.userId,
|
|
123
|
+
country: user.country,
|
|
124
|
+
preferences: {
|
|
125
|
+
theme: "dark",
|
|
126
|
+
notifications: true,
|
|
127
|
+
},
|
|
128
|
+
lastLogin: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
).pipe(Layer.provide(CloudflareAccessLive));
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Implement the Admin group with admin-only access
|
|
136
|
+
*/
|
|
137
|
+
const AdminLive = HttpApiBuilder.group(Api, "admin", (handlers) =>
|
|
138
|
+
handlers
|
|
139
|
+
.handle("getStats", () =>
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const user = yield* CurrentUser;
|
|
142
|
+
const allowedAdmins = ["admin@example.com"];
|
|
143
|
+
|
|
144
|
+
if (!allowedAdmins.includes(user.email)) {
|
|
145
|
+
return yield* Effect.fail(
|
|
146
|
+
new Forbidden({
|
|
147
|
+
message: "Admin access required",
|
|
148
|
+
email: user.email,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
totalUsers: 1500,
|
|
155
|
+
activeSessions: 342,
|
|
156
|
+
apiRequests: 125000,
|
|
157
|
+
recentActivity: ["User login", "Config update"],
|
|
158
|
+
};
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
161
|
+
.handle("getUsers", () =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
const user = yield* CurrentUser;
|
|
164
|
+
const allowedAdmins = ["admin@example.com"];
|
|
165
|
+
|
|
166
|
+
if (!allowedAdmins.includes(user.email)) {
|
|
167
|
+
return yield* Effect.fail(
|
|
168
|
+
new Forbidden({
|
|
169
|
+
message: "Admin access required",
|
|
170
|
+
email: user.email,
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [
|
|
176
|
+
{ email: "user1@example.com", userId: "1" },
|
|
177
|
+
{ email: "user2@example.com", userId: "2" },
|
|
178
|
+
];
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
).pipe(Layer.provide(CloudflareAccessLive));
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Implement the Public group (no auth required)
|
|
185
|
+
*/
|
|
186
|
+
const PublicLive = HttpApiBuilder.group(Api, "public", (handlers) =>
|
|
187
|
+
handlers.handle("health", () => Effect.succeed({ status: "healthy" })),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// 4. SERVER SETUP
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
const ApiLive = HttpApiBuilder.api(Api).pipe(
|
|
195
|
+
Layer.provide(UsersLive),
|
|
196
|
+
Layer.provide(AdminLive),
|
|
197
|
+
Layer.provide(PublicLive),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
HttpApiBuilder.serve().pipe(
|
|
201
|
+
Layer.provide(ApiLive),
|
|
202
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
|
|
203
|
+
Layer.launch,
|
|
204
|
+
NodeRuntime.runMain,
|
|
205
|
+
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Allowlist Example
|
|
3
|
+
*
|
|
4
|
+
* Restrict access to specific email addresses. This provides an additional
|
|
5
|
+
* layer of security beyond Cloudflare Access policies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import {
|
|
10
|
+
createCloudflareAccessAuth,
|
|
11
|
+
getCloudflareAccessConfigFromBindings,
|
|
12
|
+
type CloudflareAccessUser,
|
|
13
|
+
} from "cloudflare-access/hono";
|
|
14
|
+
|
|
15
|
+
interface Bindings {
|
|
16
|
+
CF_ACCESS_TEAM_DOMAIN: string;
|
|
17
|
+
CF_ACCESS_AUD: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Variables {
|
|
21
|
+
user?: CloudflareAccessUser;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
25
|
+
|
|
26
|
+
// Only allow specific emails
|
|
27
|
+
const ALLOWED_EMAILS = ["admin@company.com", "ceo@company.com", "cto@company.com"];
|
|
28
|
+
|
|
29
|
+
app.use(
|
|
30
|
+
createCloudflareAccessAuth({
|
|
31
|
+
accessConfig: getCloudflareAccessConfigFromBindings,
|
|
32
|
+
allowedEmails: ALLOWED_EMAILS,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Only accessible to allowed emails
|
|
37
|
+
app.get("/api/sensitive-data", (c) => {
|
|
38
|
+
const user = c.get("user");
|
|
39
|
+
|
|
40
|
+
return c.json({
|
|
41
|
+
message: "Access granted to sensitive data",
|
|
42
|
+
user: user?.email,
|
|
43
|
+
data: {
|
|
44
|
+
revenue: 1000000,
|
|
45
|
+
projections: "classified",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export default app;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { cloudflareAccessAuth } from "cloudflare-access/express";
|
|
3
|
+
|
|
4
|
+
const app = express();
|
|
5
|
+
|
|
6
|
+
// Use Cloudflare Access middleware
|
|
7
|
+
app.use(
|
|
8
|
+
cloudflareAccessAuth({
|
|
9
|
+
accessConfig: {
|
|
10
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
11
|
+
audTag: "your-audience-tag",
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
app.get("/protected", (req, res) => {
|
|
17
|
+
res.json({
|
|
18
|
+
message: `Hello ${req.user?.email}`,
|
|
19
|
+
userId: req.user?.userId,
|
|
20
|
+
country: req.user?.country,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.listen(3000, () => {
|
|
25
|
+
console.log("Server running on http://localhost:3000");
|
|
26
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fastify from "fastify";
|
|
2
|
+
import { cloudflareAccessPlugin } from "cloudflare-access/fastify";
|
|
3
|
+
|
|
4
|
+
const app = fastify();
|
|
5
|
+
|
|
6
|
+
// Register Cloudflare Access plugin
|
|
7
|
+
app.register(cloudflareAccessPlugin, {
|
|
8
|
+
accessConfig: {
|
|
9
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
10
|
+
audTag: "your-audience-tag",
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.get("/protected", async (request, _reply) => {
|
|
15
|
+
return {
|
|
16
|
+
message: `Hello ${request.user?.email}`,
|
|
17
|
+
userId: request.user?.userId,
|
|
18
|
+
country: request.user?.country,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.listen({ port: 3000 }, () => {
|
|
23
|
+
console.log("Server running on http://localhost:3000");
|
|
24
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
createCloudflareAccessAuth,
|
|
4
|
+
getCloudflareAccessConfigFromBindings,
|
|
5
|
+
type CloudflareAccessVariables,
|
|
6
|
+
} from "cloudflare-access/hono";
|
|
7
|
+
|
|
8
|
+
const app = new Hono<{ Variables: CloudflareAccessVariables }>();
|
|
9
|
+
|
|
10
|
+
// Use with Cloudflare Access bindings
|
|
11
|
+
app.use(
|
|
12
|
+
createCloudflareAccessAuth({
|
|
13
|
+
accessConfig: getCloudflareAccessConfigFromBindings,
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
app.get("/protected", (c) => {
|
|
18
|
+
const user = c.get("user");
|
|
19
|
+
return c.json({
|
|
20
|
+
message: `Hello ${user?.email}`,
|
|
21
|
+
userId: user?.userId,
|
|
22
|
+
country: user?.country,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default app;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono Router Example
|
|
3
|
+
*
|
|
4
|
+
* Using the middleware with Hono's router pattern for modular APIs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import {
|
|
9
|
+
createCloudflareAccessAuth,
|
|
10
|
+
getCloudflareAccessConfigFromBindings,
|
|
11
|
+
type CloudflareAccessUser,
|
|
12
|
+
} from "cloudflare-access/hono";
|
|
13
|
+
|
|
14
|
+
interface Bindings {
|
|
15
|
+
CF_ACCESS_TEAM_DOMAIN: string;
|
|
16
|
+
CF_ACCESS_AUD: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Extend Hono's variables type
|
|
20
|
+
interface Variables {
|
|
21
|
+
user?: CloudflareAccessUser;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create typed Hono app
|
|
25
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
26
|
+
|
|
27
|
+
// API v1 routes
|
|
28
|
+
const apiV1 = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
29
|
+
|
|
30
|
+
apiV1.use(
|
|
31
|
+
createCloudflareAccessAuth({
|
|
32
|
+
accessConfig: getCloudflareAccessConfigFromBindings,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
apiV1.get("/users/me", (c) => {
|
|
37
|
+
const user = c.get("user");
|
|
38
|
+
return c.json({
|
|
39
|
+
id: user?.userId,
|
|
40
|
+
email: user?.email,
|
|
41
|
+
country: user?.country,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
apiV1.get("/dashboard", (c) => {
|
|
46
|
+
const user = c.get("user");
|
|
47
|
+
return c.json({
|
|
48
|
+
welcome: `Hello ${user?.email}`,
|
|
49
|
+
lastLogin: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Public routes
|
|
54
|
+
const publicRouter = new Hono();
|
|
55
|
+
|
|
56
|
+
publicRouter.get("/status", (c) => {
|
|
57
|
+
return c.json({
|
|
58
|
+
status: "operational",
|
|
59
|
+
version: "1.0.0",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Mount routers
|
|
64
|
+
app.route("/api/v1", apiV1);
|
|
65
|
+
app.route("/public", publicRouter);
|
|
66
|
+
|
|
67
|
+
export default app;
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
Route structure:
|
|
71
|
+
- GET /api/v1/users/me -> Protected (requires auth)
|
|
72
|
+
- GET /api/v1/dashboard -> Protected (requires auth)
|
|
73
|
+
- GET /public/status -> Public (no auth)
|
|
74
|
+
*/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Controller, Get, Module, Req } from "@nestjs/common";
|
|
2
|
+
import type { Request } from "express";
|
|
3
|
+
import { CloudflareAccessGuard, Public } from "cloudflare-access/nestjs";
|
|
4
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
5
|
+
|
|
6
|
+
@Controller("api")
|
|
7
|
+
export class ApiController {
|
|
8
|
+
@Get("protected")
|
|
9
|
+
getProtected(@Req() req: Request) {
|
|
10
|
+
return {
|
|
11
|
+
message: `Hello ${req.user?.email}`,
|
|
12
|
+
userId: req.user?.userId,
|
|
13
|
+
country: req.user?.country,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Public()
|
|
18
|
+
@Get("public")
|
|
19
|
+
getPublic() {
|
|
20
|
+
return { message: "This is a public endpoint" };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Module({
|
|
25
|
+
controllers: [ApiController],
|
|
26
|
+
providers: [
|
|
27
|
+
{
|
|
28
|
+
provide: APP_GUARD,
|
|
29
|
+
useFactory: () =>
|
|
30
|
+
new CloudflareAccessGuard({
|
|
31
|
+
accessConfig: {
|
|
32
|
+
teamDomain: "https://yourteam.cloudflareaccess.com",
|
|
33
|
+
audTag: "your-audience-tag",
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
export class AppModule {}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip Dev Mode Example
|
|
3
|
+
*
|
|
4
|
+
* Skip authentication during local development while still
|
|
5
|
+
* requiring it in production.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import {
|
|
10
|
+
createCloudflareAccessAuth,
|
|
11
|
+
getCloudflareAccessConfigFromBindings,
|
|
12
|
+
type CloudflareAccessUser,
|
|
13
|
+
} from "cloudflare-access/hono";
|
|
14
|
+
|
|
15
|
+
interface Bindings {
|
|
16
|
+
CF_ACCESS_TEAM_DOMAIN: string;
|
|
17
|
+
CF_ACCESS_AUD: string;
|
|
18
|
+
ENVIRONMENT: "dev" | "staging" | "prod";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Variables {
|
|
22
|
+
user?: CloudflareAccessUser;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
|
|
26
|
+
|
|
27
|
+
app.use(
|
|
28
|
+
createCloudflareAccessAuth({
|
|
29
|
+
accessConfig: getCloudflareAccessConfigFromBindings,
|
|
30
|
+
|
|
31
|
+
// When true, auth is skipped for localhost/127.0.0.1
|
|
32
|
+
// when ENVIRONMENT is not "prod"
|
|
33
|
+
skipInDev: true,
|
|
34
|
+
|
|
35
|
+
// Also exclude specific paths from auth
|
|
36
|
+
excludePaths: ["/api/public", "/health"],
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// This route:
|
|
41
|
+
// - Requires auth in production
|
|
42
|
+
// - Skips auth on localhost in dev/staging
|
|
43
|
+
app.get("/api/private", (c) => {
|
|
44
|
+
const user = c.get("user");
|
|
45
|
+
const env = c.env.ENVIRONMENT;
|
|
46
|
+
|
|
47
|
+
// In dev mode, user might be undefined since auth is skipped
|
|
48
|
+
if (!user) {
|
|
49
|
+
return c.json({
|
|
50
|
+
message: "Development mode - auth skipped",
|
|
51
|
+
environment: env,
|
|
52
|
+
note: "In production, this would require authentication",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return c.json({
|
|
57
|
+
message: "Production mode - authenticated",
|
|
58
|
+
environment: env,
|
|
59
|
+
user: user?.email,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Always public
|
|
64
|
+
app.get("/health", (c) => {
|
|
65
|
+
return c.json({ status: "ok" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export default app;
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
## Development workflow:
|
|
72
|
+
|
|
73
|
+
1. Local development (no auth required):
|
|
74
|
+
```bash
|
|
75
|
+
ENVIRONMENT=dev bun run dev
|
|
76
|
+
curl http://localhost:8787/api/private
|
|
77
|
+
# Returns: { message: "Development mode..." }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. Production (auth required):
|
|
81
|
+
```bash
|
|
82
|
+
# Deployed to Cloudflare with ENVIRONMENT=prod
|
|
83
|
+
curl https://api.example.com/api/private
|
|
84
|
+
# Returns: 401 Unauthorized
|
|
85
|
+
|
|
86
|
+
curl -H "CF-Access-JWT-Assertion: <token>" https://api.example.com/api/private
|
|
87
|
+
# Returns: { message: "Production mode...", user: "..." }
|
|
88
|
+
```
|
|
89
|
+
*/
|