Guest User

Untitled

a guest
May 17th, 2025
158
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.98 KB | None | 0 0
  1. Okay, this is a fun challenge! We'll create a simple Todo app using:
  2.  
  3. 1. **Cloudflare Workers:** For the serverless backend logic and serving the HTML.
  4. 2. **Cloudflare KV Store:** To store todo items per user.
  5. 3. **Auth0:** For authentication.
  6. 4. **Vanilla HTML, CSS, and JavaScript:** For the frontend.
  7.  
  8. **Limitations for a "Simple" Example:**
  9. * No complex JWT validation libraries (we'll trust Auth0 and use the ID token as our session). For production, you'd want robust JWT validation.
  10. * Error handling will be basic.
  11. * Styling will be minimal.
  12.  
  13. ---
  14.  
  15. **Step 1: Auth0 Setup**
  16.  
  17. 1. **Sign up/Login to Auth0.**
  18. 2. Go to **Applications** -> **Applications** and click **"Create Application"**.
  19. 3. Choose **"Regular Web Applications"** and give it a name (e.g., "Cloudflare Todo App"). Click **Create**.
  20. 4. Go to the **Settings** tab for your new application.
  21. 5. Note down:
  22. * **Domain**
  23. * **Client ID**
  24. * **Client Secret**
  25. 6. In the **Application URIs** section:
  26. * **Allowed Callback URLs:** Set this to `https://<YOUR_WORKER_SUBDOMAIN>.<YOUR_ACCOUNT_ID>.workers.dev/callback`
  27. * Replace `<YOUR_WORKER_SUBDOMAIN>` with the name you'll give your worker (e.g., `todo-app`).
  28. * Replace `<YOUR_ACCOUNT_ID>` with your Cloudflare Account ID (find it on the right sidebar of your Cloudflare dashboard).
  29. * **Allowed Logout URLs:** Set this to `https://<YOUR_WORKER_SUBDOMAIN>.<YOUR_ACCOUNT_ID>.workers.dev/`
  30. * **Allowed Web Origins:** Set this to `https://<YOUR_WORKER_SUBDOMAIN>.<YOUR_ACCOUNT_ID>.workers.dev`
  31. 7. Scroll down and click **"Save Changes"**.
  32.  
  33. ---
  34.  
  35. **Step 2: Cloudflare Setup**
  36.  
  37. 1. **Install Wrangler CLI:**
  38. ```bash
  39. npm install -g wrangler
  40. wrangler login # Authenticate with your Cloudflare account
  41. ```
  42.  
  43. 2. **Create a new Worker project:**
  44. ```bash
  45. wrangler init todo-auth0-app
  46. cd todo-auth0-app
  47. ```
  48. When prompted, choose "Fetch handler" (or similar for a basic worker). Don't select a template that uses modules yet, for simplicity.
  49.  
  50. 3. **Create a KV Namespace:**
  51. ```bash
  52. wrangler kv:namespace create TODOS_KV
  53. ```
  54. This will output something like:
  55. ```
  56. ๐ŸŒ€ Creating namespace "TODOS_KV"
  57. โœจ Success!
  58. Add the following to your wrangler.toml:
  59. kv_namespaces = [
  60. { binding = "TODOS_KV", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
  61. ]
  62. ```
  63. Copy the `kv_namespaces` part into your `wrangler.toml` file.
  64.  
  65. 4. **Configure `wrangler.toml`:**
  66. Your `wrangler.toml` should look something like this:
  67. ```toml
  68. name = "todo-auth0-app" # Or whatever you want your worker subdomain to be
  69. main = "src/index.js"
  70. compatibility_date = "2023-10-30" # Or a recent date
  71.  
  72. # Add these secrets (we'll set them in the dashboard or via CLI)
  73. # [vars]
  74. # AUTH0_DOMAIN = "your-auth0-domain.auth0.com"
  75. # AUTH0_CLIENT_ID = "your-auth0-client-id"
  76. # AUTH0_CLIENT_SECRET = "your-auth0-client-secret"
  77. # COOKIE_SECRET = "a-very-strong-random-secret-for-cookies" # Generate a random string
  78.  
  79. kv_namespaces = [
  80. { binding = "TODOS_KV", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } # The ID from the previous step
  81. ]
  82. ```
  83. * **Important:** Generate a strong random string for `COOKIE_SECRET`.
  84. * We will set the `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, and `COOKIE_SECRET` as secrets in the Cloudflare dashboard or via Wrangler CLI for security.
  85.  
  86. 5. **Set Secrets in Cloudflare:**
  87. Go to your Cloudflare Dashboard -> Workers & Pages -> Select your worker (once deployed) -> Settings -> Variables -> Add secrets for:
  88. * `AUTH0_DOMAIN`
  89. * `AUTH0_CLIENT_ID`
  90. * `AUTH0_CLIENT_SECRET`
  91. * `COOKIE_SECRET`
  92. Alternatively, using Wrangler CLI (before the first deploy, you might need to deploy a placeholder first):
  93. ```bash
  94. echo "your-auth0-domain.auth0.com" | wrangler secret put AUTH0_DOMAIN
  95. echo "your-auth0-client-id" | wrangler secret put AUTH0_CLIENT_ID
  96. echo "your-auth0-client-secret" | wrangler secret put AUTH0_CLIENT_SECRET
  97. echo "generate-a-strong-random-string-here" | wrangler secret put COOKIE_SECRET
  98. ```
  99.  
  100. ---
  101.  
  102. **Step 3: Worker Code (`src/index.js`)**
  103.  
  104. This will be a single file handling routing, Auth0 flow, and API.
  105.  
  106. ```javascript
  107. // src/index.js
  108.  
  109. // Simple router (you might use a library like itty-router for more complex apps)
  110. const Router = () => {
  111. const routes = [];
  112. const add = (method, path, handler) => routes.push({ method, path, handler });
  113. const handle = async (request, env, ctx) => {
  114. const url = new URL(request.url);
  115. for (const route of routes) {
  116. if (request.method === route.method) {
  117. const match = url.pathname.match(new RegExp(`^${route.path.replace(/:\w+/g, '([^/]+)')}$`));
  118. if (match) {
  119. const params = {};
  120. const keys = (route.path.match(/:\w+/g) || []).map(key => key.substring(1));
  121. keys.forEach((key, i) => params[key] = match[i+1]);
  122. request.params = params;
  123. return route.handler(request, env, ctx);
  124. }
  125. }
  126. }
  127. return new Response("Not Found", { status: 404 });
  128. };
  129. return { get: (p, h) => add("GET", p, h), post: (p, h) => add("POST", p, h), delete: (p, h) => add("DELETE", p, h), handle };
  130. };
  131.  
  132. const router = Router();
  133.  
  134. // --- Configuration (from environment variables/secrets) ---
  135. let AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, COOKIE_SECRET, APP_URL;
  136.  
  137. function initializeEnv(env) {
  138. AUTH0_DOMAIN = env.AUTH0_DOMAIN;
  139. AUTH0_CLIENT_ID = env.AUTH0_CLIENT_ID;
  140. AUTH0_CLIENT_SECRET = env.AUTH0_CLIENT_SECRET;
  141. COOKIE_SECRET = env.COOKIE_SECRET; // Used for signing state, not strictly necessary for this simple example with ID token
  142. }
  143.  
  144. // --- HTML Content ---
  145. const htmlPage = (bodyContent, user) => `
  146. <!DOCTYPE html>
  147. <html lang="en">
  148. <head>
  149. <meta charset="UTF-8">
  150. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  151. <title>Cloudflare Todo App</title>
  152. <style>
  153. body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
  154. .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
  155. h1 { color: #333; }
  156. input[type="text"] { padding: 10px; margin-right: 10px; border: 1px solid #ddd; border-radius: 4px; }
  157. button { padding: 10px 15px; background-color: #5cb85c; color: white; border: none; border-radius: 4px; cursor: pointer; }
  158. button:hover { background-color: #4cae4c; }
  159. .logout-btn { background-color: #d9534f; }
  160. .logout-btn:hover { background-color: #c9302c; }
  161. ul { list-style-type: none; padding: 0; }
  162. li { background-color: #e9e9e9; margin-bottom: 8px; padding: 10px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
  163. li button { background-color: #f0ad4e; font-size: 0.8em; padding: 5px 10px; }
  164. li button:hover { background-color: #ec971f; }
  165. .auth-buttons { margin-bottom: 20px; }
  166. </style>
  167. </head>
  168. <body>
  169. <div class="container">
  170. <h1>Todo List</h1>
  171. ${bodyContent}
  172. </div>
  173. <script>
  174. // Client-side JavaScript will be minimal for this example,
  175. // but could be expanded for better UX.
  176. async function addTodo() {
  177. const input = document.getElementById('todo-text');
  178. const text = input.value.trim();
  179. if (!text) return;
  180. const response = await fetch('/api/todos', {
  181. method: 'POST',
  182. headers: { 'Content-Type': 'application/json' },
  183. body: JSON.stringify({ text })
  184. });
  185. if (response.ok) {
  186. input.value = '';
  187. location.reload(); // Simple reload to refresh list
  188. } else {
  189. alert('Failed to add todo.');
  190. }
  191. }
  192.  
  193. async function deleteTodo(id) {
  194. if (!confirm('Are you sure you want to delete this todo?')) return;
  195. const response = await fetch('/api/todos/' + id, {
  196. method: 'DELETE'
  197. });
  198. if (response.ok) {
  199. location.reload(); // Simple reload
  200. } else {
  201. alert('Failed to delete todo.');
  202. }
  203. }
  204. </script>
  205. </body>
  206. </html>
  207. `;
  208.  
  209. // --- Auth Middleware ---
  210. async function getUser(request, env) {
  211. const cookieHeader = request.headers.get('Cookie');
  212. if (cookieHeader && cookieHeader.includes('auth_token=')) {
  213. const cookies = Object.fromEntries(cookieHeader.split(';').map(c => c.trim().split('=')));
  214. const token = cookies.auth_token;
  215. if (token) {
  216. try {
  217. // WARNING: In a real app, you MUST validate the JWT signature and claims (issuer, audience, expiry)
  218. // For simplicity here, we are decoding and trusting it.
  219. // You would use crypto.subtle.verify with JWKS from Auth0.
  220. const payload = JSON.parse(atob(token.split('.')[1])); // Decode payload
  221. if (payload.exp * 1000 < Date.now()) {
  222. console.log("Token expired");
  223. return null; // Token expired
  224. }
  225. // Check issuer
  226. // if (payload.iss !== `https://${AUTH0_DOMAIN}/`) {
  227. // console.log("Invalid issuer");
  228. // return null;
  229. // }
  230. // Check audience
  231. // if (payload.aud !== AUTH0_CLIENT_ID) {
  232. // console.log("Invalid audience");
  233. // return null;
  234. // }
  235. return { id: payload.sub, email: payload.email, name: payload.name }; // Auth0 'sub' is the user ID
  236. } catch (e) {
  237. console.error("Token validation/parsing error:", e);
  238. return null;
  239. }
  240. }
  241. }
  242. return null;
  243. }
  244.  
  245. // --- Routes ---
  246.  
  247. // Homepage: Show login or todo list
  248. router.get("/", async (request, env) => {
  249. APP_URL = new URL(request.url).origin; // Set APP_URL dynamically
  250. const user = await getUser(request, env);
  251.  
  252. if (user) {
  253. const todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
  254. const todoListItems = todos.map(todo =>
  255. `<li>${todo.text} <button onclick="deleteTodo('${todo.id}')">Delete</button></li>`
  256. ).join('');
  257.  
  258. const body = `
  259. <p>Welcome, ${user.name || user.email}!</p>
  260. <a href="/logout"><button class="logout-btn">Logout</button></a>
  261. <div>
  262. <input type="text" id="todo-text" placeholder="New todo...">
  263. <button onclick="addTodo()">Add Todo</button>
  264. </div>
  265. <ul>${todoListItems}</ul>
  266. `;
  267. return new Response(htmlPage(body, user), { headers: { 'Content-Type': 'text/html' } });
  268. } else {
  269. const body = `
  270. <div class="auth-buttons">
  271. <p>Please log in to manage your todos.</p>
  272. <a href="/login"><button>Login with Auth0</button></a>
  273. </div>
  274. `;
  275. return new Response(htmlPage(body, null), { headers: { 'Content-Type': 'text/html' } });
  276. }
  277. });
  278.  
  279. // Login: Redirect to Auth0
  280. router.get("/login", async (request, env) => {
  281. const state = Math.random().toString(36).substring(2); // Simple CSRF token
  282. // In a real app, store state in a short-lived cookie and verify on callback
  283.  
  284. const authUrl = new URL(`https://${AUTH0_DOMAIN}/authorize`);
  285. authUrl.searchParams.set('response_type', 'code');
  286. authUrl.searchParams.set('client_id', AUTH0_CLIENT_ID);
  287. authUrl.searchParams.set('redirect_uri', `${APP_URL}/callback`);
  288. authUrl.searchParams.set('scope', 'openid profile email');
  289. authUrl.searchParams.set('state', state);
  290.  
  291. return Response.redirect(authUrl.toString(), 302);
  292. });
  293.  
  294. // Callback: Auth0 redirects here after login
  295. router.get("/callback", async (request, env) => {
  296. const url = new URL(request.url);
  297. const code = url.searchParams.get('code');
  298. const state = url.searchParams.get('state'); // Verify state here if you stored it
  299.  
  300. if (!code) {
  301. return new Response("Missing authorization code", { status: 400 });
  302. }
  303.  
  304. const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
  305. method: 'POST',
  306. headers: { 'Content-Type': 'application/json' },
  307. body: JSON.stringify({
  308. grant_type: 'authorization_code',
  309. client_id: AUTH0_CLIENT_ID,
  310. client_secret: AUTH0_CLIENT_SECRET,
  311. code: code,
  312. redirect_uri: `${APP_URL}/callback`
  313. })
  314. });
  315.  
  316. if (!tokenResponse.ok) {
  317. const errorBody = await tokenResponse.text();
  318. console.error("Token exchange error:", errorBody);
  319. return new Response(`Error fetching token: ${errorBody}`, { status: 500 });
  320. }
  321.  
  322. const { id_token, access_token } = await tokenResponse.json();
  323.  
  324. // Set ID token in an HttpOnly cookie. Secure should be true in production (HTTPS).
  325. // For simplicity, we're not using a session store; the ID token itself acts as the session.
  326. // Max-Age: 1 day (Auth0 default ID token expiry is usually shorter, but cookie can live longer)
  327. const cookieOptions = `HttpOnly; Path=/; Max-Age=86400; SameSite=Lax; ${APP_URL.startsWith('https://') ? 'Secure;' : ''}`;
  328. const headers = new Headers({
  329. 'Location': '/',
  330. 'Set-Cookie': `auth_token=${id_token}; ${cookieOptions}`
  331. });
  332.  
  333. return new Response(null, { status: 302, headers });
  334. });
  335.  
  336. // Logout
  337. router.get("/logout", async (request, env) => {
  338. const cookieOptions = `HttpOnly; Path=/; Max-Age=0; SameSite=Lax; ${APP_URL.startsWith('https://') ? 'Secure;' : ''}`; // Expire cookie
  339. const headers = new Headers({
  340. 'Set-Cookie': `auth_token=; ${cookieOptions}`
  341. });
  342.  
  343. // Redirect to Auth0 logout, then back to app's root
  344. const logoutUrl = new URL(`https://${AUTH0_DOMAIN}/v2/logout`);
  345. logoutUrl.searchParams.set('client_id', AUTH0_CLIENT_ID);
  346. logoutUrl.searchParams.set('returnTo', APP_URL);
  347. headers.set('Location', logoutUrl.toString());
  348.  
  349. return new Response(null, { status: 302, headers });
  350. });
  351.  
  352.  
  353. // --- API Routes (Protected) ---
  354.  
  355. // GET Todos
  356. router.get("/api/todos", async (request, env) => {
  357. const user = await getUser(request, env);
  358. if (!user) return new Response("Unauthorized", { status: 401 });
  359.  
  360. const todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
  361. return new Response(JSON.stringify(todos), { headers: { 'Content-Type': 'application/json' } });
  362. });
  363.  
  364. // POST Add Todo
  365. router.post("/api/todos", async (request, env) => {
  366. const user = await getUser(request, env);
  367. if (!user) return new Response("Unauthorized", { status: 401 });
  368.  
  369. try {
  370. const { text } = await request.json();
  371. if (!text || typeof text !== 'string' || text.trim() === '') {
  372. return new Response("Invalid todo text", { status: 400 });
  373. }
  374.  
  375. const todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
  376. const newTodo = { id: crypto.randomUUID(), text: text.trim(), completed: false };
  377. todos.push(newTodo);
  378. await env.TODOS_KV.put(user.id + "_todos", JSON.stringify(todos));
  379. return new Response(JSON.stringify(newTodo), { status: 201, headers: { 'Content-Type': 'application/json' } });
  380. } catch (e) {
  381. return new Response("Bad request body", { status: 400 });
  382. }
  383. });
  384.  
  385. // DELETE Todo
  386. router.delete("/api/todos/:id", async (request, env) => {
  387. const user = await getUser(request, env);
  388. if (!user) return new Response("Unauthorized", { status: 401 });
  389.  
  390. const todoId = request.params.id;
  391. let todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
  392. const initialLength = todos.length;
  393. todos = todos.filter(todo => todo.id !== todoId);
  394.  
  395. if (todos.length === initialLength) {
  396. return new Response("Todo not found", { status: 404 });
  397. }
  398.  
  399. await env.TODOS_KV.put(user.id + "_todos", JSON.stringify(todos));
  400. return new Response(null, { status: 204 });
  401. });
  402.  
  403.  
  404. // --- Worker Entry Point ---
  405. export default {
  406. async fetch(request, env, ctx) {
  407. initializeEnv(env); // Make secrets available globally in this request scope
  408. APP_URL = new URL(request.url).origin; // Ensure APP_URL is set for each request
  409. return router.handle(request, env, ctx);
  410. }
  411. };
  412. ```
  413.  
  414. **Important Security Note on `getUser` function:**
  415. The provided `getUser` function decodes the JWT payload but **does not validate its signature or all claims (like issuer, audience, exact expiry check against `nbf`)**. In a production application, you **MUST** validate the JWT properly. This typically involves:
  416. 1. Fetching Auth0's JWKS (JSON Web Key Set) from `https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json`.
  417. 2. Finding the key in the JWKS that matches the `kid` (Key ID) in the JWT header.
  418. 3. Using `crypto.subtle.verify()` with the public key to verify the token's signature.
  419. 4. Validating claims like `iss` (issuer), `aud` (audience), and `exp` (expiration time).
  420. Cloudflare Workers support `crypto.subtle`, so this is feasible. For brevity in this "simple" example, it's omitted but is critical for security.
  421.  
  422. ---
  423.  
  424. **Step 4: Deployment**
  425.  
  426. 1. **Build (if necessary, though not for this simple setup) and Deploy:**
  427. ```bash
  428. wrangler deploy
  429. ```
  430. This will output the URL of your deployed worker (e.g., `https://todo-auth0-app.your-account.workers.dev`). Make sure this matches what you configured in Auth0's "Allowed Callback URLs", "Allowed Logout URLs", and "Allowed Web Origins".
  431.  
  432. 2. **Test:**
  433. * Open the deployed URL in your browser.
  434. * You should see the login button.
  435. * Click login, authenticate with Auth0.
  436. * You should be redirected back to the app, logged in.
  437. * Try adding, listing, and deleting todos.
  438. * Try logging out.
  439.  
  440. ---
  441.  
  442. This provides a foundational, simple todo app with Auth0 authentication on Cloudflare. For a production system, you'd want to:
  443. * Implement robust JWT validation.
  444. * Add more comprehensive error handling.
  445. * Improve the UI/UX (perhaps with a frontend framework or more advanced vanilla JS).
  446. * Consider pagination or more advanced KV store usage if lists get very long.
  447. * Use a proper router library if your app grows.
  448. * Implement proper CSRF protection for the `/callback` using the `state` parameter by storing it in a short-lived, HttpOnly cookie.
  449. * Set the `Secure` flag on cookies when served over HTTPS (which Cloudflare Workers always are by default).
Advertisement
Add Comment
Please, Sign In to add comment