Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Okay, this is a fun challenge! We'll create a simple Todo app using:
- 1. **Cloudflare Workers:** For the serverless backend logic and serving the HTML.
- 2. **Cloudflare KV Store:** To store todo items per user.
- 3. **Auth0:** For authentication.
- 4. **Vanilla HTML, CSS, and JavaScript:** For the frontend.
- **Limitations for a "Simple" Example:**
- * 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.
- * Error handling will be basic.
- * Styling will be minimal.
- ---
- **Step 1: Auth0 Setup**
- 1. **Sign up/Login to Auth0.**
- 2. Go to **Applications** -> **Applications** and click **"Create Application"**.
- 3. Choose **"Regular Web Applications"** and give it a name (e.g., "Cloudflare Todo App"). Click **Create**.
- 4. Go to the **Settings** tab for your new application.
- 5. Note down:
- * **Domain**
- * **Client ID**
- * **Client Secret**
- 6. In the **Application URIs** section:
- * **Allowed Callback URLs:** Set this to `https://<YOUR_WORKER_SUBDOMAIN>.<YOUR_ACCOUNT_ID>.workers.dev/callback`
- * Replace `<YOUR_WORKER_SUBDOMAIN>` with the name you'll give your worker (e.g., `todo-app`).
- * Replace `<YOUR_ACCOUNT_ID>` with your Cloudflare Account ID (find it on the right sidebar of your Cloudflare dashboard).
- * **Allowed Logout URLs:** Set this to `https://<YOUR_WORKER_SUBDOMAIN>.<YOUR_ACCOUNT_ID>.workers.dev/`
- * **Allowed Web Origins:** Set this to `https://<YOUR_WORKER_SUBDOMAIN>.<YOUR_ACCOUNT_ID>.workers.dev`
- 7. Scroll down and click **"Save Changes"**.
- ---
- **Step 2: Cloudflare Setup**
- 1. **Install Wrangler CLI:**
- ```bash
- npm install -g wrangler
- wrangler login # Authenticate with your Cloudflare account
- ```
- 2. **Create a new Worker project:**
- ```bash
- wrangler init todo-auth0-app
- cd todo-auth0-app
- ```
- When prompted, choose "Fetch handler" (or similar for a basic worker). Don't select a template that uses modules yet, for simplicity.
- 3. **Create a KV Namespace:**
- ```bash
- wrangler kv:namespace create TODOS_KV
- ```
- This will output something like:
- ```
- ๐ Creating namespace "TODOS_KV"
- โจ Success!
- Add the following to your wrangler.toml:
- kv_namespaces = [
- { binding = "TODOS_KV", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
- ]
- ```
- Copy the `kv_namespaces` part into your `wrangler.toml` file.
- 4. **Configure `wrangler.toml`:**
- Your `wrangler.toml` should look something like this:
- ```toml
- name = "todo-auth0-app" # Or whatever you want your worker subdomain to be
- main = "src/index.js"
- compatibility_date = "2023-10-30" # Or a recent date
- # Add these secrets (we'll set them in the dashboard or via CLI)
- # [vars]
- # AUTH0_DOMAIN = "your-auth0-domain.auth0.com"
- # AUTH0_CLIENT_ID = "your-auth0-client-id"
- # AUTH0_CLIENT_SECRET = "your-auth0-client-secret"
- # COOKIE_SECRET = "a-very-strong-random-secret-for-cookies" # Generate a random string
- kv_namespaces = [
- { binding = "TODOS_KV", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } # The ID from the previous step
- ]
- ```
- * **Important:** Generate a strong random string for `COOKIE_SECRET`.
- * 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.
- 5. **Set Secrets in Cloudflare:**
- Go to your Cloudflare Dashboard -> Workers & Pages -> Select your worker (once deployed) -> Settings -> Variables -> Add secrets for:
- * `AUTH0_DOMAIN`
- * `AUTH0_CLIENT_ID`
- * `AUTH0_CLIENT_SECRET`
- * `COOKIE_SECRET`
- Alternatively, using Wrangler CLI (before the first deploy, you might need to deploy a placeholder first):
- ```bash
- echo "your-auth0-domain.auth0.com" | wrangler secret put AUTH0_DOMAIN
- echo "your-auth0-client-id" | wrangler secret put AUTH0_CLIENT_ID
- echo "your-auth0-client-secret" | wrangler secret put AUTH0_CLIENT_SECRET
- echo "generate-a-strong-random-string-here" | wrangler secret put COOKIE_SECRET
- ```
- ---
- **Step 3: Worker Code (`src/index.js`)**
- This will be a single file handling routing, Auth0 flow, and API.
- ```javascript
- // src/index.js
- // Simple router (you might use a library like itty-router for more complex apps)
- const Router = () => {
- const routes = [];
- const add = (method, path, handler) => routes.push({ method, path, handler });
- const handle = async (request, env, ctx) => {
- const url = new URL(request.url);
- for (const route of routes) {
- if (request.method === route.method) {
- const match = url.pathname.match(new RegExp(`^${route.path.replace(/:\w+/g, '([^/]+)')}$`));
- if (match) {
- const params = {};
- const keys = (route.path.match(/:\w+/g) || []).map(key => key.substring(1));
- keys.forEach((key, i) => params[key] = match[i+1]);
- request.params = params;
- return route.handler(request, env, ctx);
- }
- }
- }
- return new Response("Not Found", { status: 404 });
- };
- return { get: (p, h) => add("GET", p, h), post: (p, h) => add("POST", p, h), delete: (p, h) => add("DELETE", p, h), handle };
- };
- const router = Router();
- // --- Configuration (from environment variables/secrets) ---
- let AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, COOKIE_SECRET, APP_URL;
- function initializeEnv(env) {
- AUTH0_DOMAIN = env.AUTH0_DOMAIN;
- AUTH0_CLIENT_ID = env.AUTH0_CLIENT_ID;
- AUTH0_CLIENT_SECRET = env.AUTH0_CLIENT_SECRET;
- COOKIE_SECRET = env.COOKIE_SECRET; // Used for signing state, not strictly necessary for this simple example with ID token
- }
- // --- HTML Content ---
- const htmlPage = (bodyContent, user) => `
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Cloudflare Todo App</title>
- <style>
- body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
- .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
- h1 { color: #333; }
- input[type="text"] { padding: 10px; margin-right: 10px; border: 1px solid #ddd; border-radius: 4px; }
- button { padding: 10px 15px; background-color: #5cb85c; color: white; border: none; border-radius: 4px; cursor: pointer; }
- button:hover { background-color: #4cae4c; }
- .logout-btn { background-color: #d9534f; }
- .logout-btn:hover { background-color: #c9302c; }
- ul { list-style-type: none; padding: 0; }
- li { background-color: #e9e9e9; margin-bottom: 8px; padding: 10px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
- li button { background-color: #f0ad4e; font-size: 0.8em; padding: 5px 10px; }
- li button:hover { background-color: #ec971f; }
- .auth-buttons { margin-bottom: 20px; }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>Todo List</h1>
- ${bodyContent}
- </div>
- <script>
- // Client-side JavaScript will be minimal for this example,
- // but could be expanded for better UX.
- async function addTodo() {
- const input = document.getElementById('todo-text');
- const text = input.value.trim();
- if (!text) return;
- const response = await fetch('/api/todos', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ text })
- });
- if (response.ok) {
- input.value = '';
- location.reload(); // Simple reload to refresh list
- } else {
- alert('Failed to add todo.');
- }
- }
- async function deleteTodo(id) {
- if (!confirm('Are you sure you want to delete this todo?')) return;
- const response = await fetch('/api/todos/' + id, {
- method: 'DELETE'
- });
- if (response.ok) {
- location.reload(); // Simple reload
- } else {
- alert('Failed to delete todo.');
- }
- }
- </script>
- </body>
- </html>
- `;
- // --- Auth Middleware ---
- async function getUser(request, env) {
- const cookieHeader = request.headers.get('Cookie');
- if (cookieHeader && cookieHeader.includes('auth_token=')) {
- const cookies = Object.fromEntries(cookieHeader.split(';').map(c => c.trim().split('=')));
- const token = cookies.auth_token;
- if (token) {
- try {
- // WARNING: In a real app, you MUST validate the JWT signature and claims (issuer, audience, expiry)
- // For simplicity here, we are decoding and trusting it.
- // You would use crypto.subtle.verify with JWKS from Auth0.
- const payload = JSON.parse(atob(token.split('.')[1])); // Decode payload
- if (payload.exp * 1000 < Date.now()) {
- console.log("Token expired");
- return null; // Token expired
- }
- // Check issuer
- // if (payload.iss !== `https://${AUTH0_DOMAIN}/`) {
- // console.log("Invalid issuer");
- // return null;
- // }
- // Check audience
- // if (payload.aud !== AUTH0_CLIENT_ID) {
- // console.log("Invalid audience");
- // return null;
- // }
- return { id: payload.sub, email: payload.email, name: payload.name }; // Auth0 'sub' is the user ID
- } catch (e) {
- console.error("Token validation/parsing error:", e);
- return null;
- }
- }
- }
- return null;
- }
- // --- Routes ---
- // Homepage: Show login or todo list
- router.get("/", async (request, env) => {
- APP_URL = new URL(request.url).origin; // Set APP_URL dynamically
- const user = await getUser(request, env);
- if (user) {
- const todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
- const todoListItems = todos.map(todo =>
- `<li>${todo.text} <button onclick="deleteTodo('${todo.id}')">Delete</button></li>`
- ).join('');
- const body = `
- <p>Welcome, ${user.name || user.email}!</p>
- <a href="/logout"><button class="logout-btn">Logout</button></a>
- <div>
- <input type="text" id="todo-text" placeholder="New todo...">
- <button onclick="addTodo()">Add Todo</button>
- </div>
- <ul>${todoListItems}</ul>
- `;
- return new Response(htmlPage(body, user), { headers: { 'Content-Type': 'text/html' } });
- } else {
- const body = `
- <div class="auth-buttons">
- <p>Please log in to manage your todos.</p>
- <a href="/login"><button>Login with Auth0</button></a>
- </div>
- `;
- return new Response(htmlPage(body, null), { headers: { 'Content-Type': 'text/html' } });
- }
- });
- // Login: Redirect to Auth0
- router.get("/login", async (request, env) => {
- const state = Math.random().toString(36).substring(2); // Simple CSRF token
- // In a real app, store state in a short-lived cookie and verify on callback
- const authUrl = new URL(`https://${AUTH0_DOMAIN}/authorize`);
- authUrl.searchParams.set('response_type', 'code');
- authUrl.searchParams.set('client_id', AUTH0_CLIENT_ID);
- authUrl.searchParams.set('redirect_uri', `${APP_URL}/callback`);
- authUrl.searchParams.set('scope', 'openid profile email');
- authUrl.searchParams.set('state', state);
- return Response.redirect(authUrl.toString(), 302);
- });
- // Callback: Auth0 redirects here after login
- router.get("/callback", async (request, env) => {
- const url = new URL(request.url);
- const code = url.searchParams.get('code');
- const state = url.searchParams.get('state'); // Verify state here if you stored it
- if (!code) {
- return new Response("Missing authorization code", { status: 400 });
- }
- const tokenResponse = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- grant_type: 'authorization_code',
- client_id: AUTH0_CLIENT_ID,
- client_secret: AUTH0_CLIENT_SECRET,
- code: code,
- redirect_uri: `${APP_URL}/callback`
- })
- });
- if (!tokenResponse.ok) {
- const errorBody = await tokenResponse.text();
- console.error("Token exchange error:", errorBody);
- return new Response(`Error fetching token: ${errorBody}`, { status: 500 });
- }
- const { id_token, access_token } = await tokenResponse.json();
- // Set ID token in an HttpOnly cookie. Secure should be true in production (HTTPS).
- // For simplicity, we're not using a session store; the ID token itself acts as the session.
- // Max-Age: 1 day (Auth0 default ID token expiry is usually shorter, but cookie can live longer)
- const cookieOptions = `HttpOnly; Path=/; Max-Age=86400; SameSite=Lax; ${APP_URL.startsWith('https://') ? 'Secure;' : ''}`;
- const headers = new Headers({
- 'Location': '/',
- 'Set-Cookie': `auth_token=${id_token}; ${cookieOptions}`
- });
- return new Response(null, { status: 302, headers });
- });
- // Logout
- router.get("/logout", async (request, env) => {
- const cookieOptions = `HttpOnly; Path=/; Max-Age=0; SameSite=Lax; ${APP_URL.startsWith('https://') ? 'Secure;' : ''}`; // Expire cookie
- const headers = new Headers({
- 'Set-Cookie': `auth_token=; ${cookieOptions}`
- });
- // Redirect to Auth0 logout, then back to app's root
- const logoutUrl = new URL(`https://${AUTH0_DOMAIN}/v2/logout`);
- logoutUrl.searchParams.set('client_id', AUTH0_CLIENT_ID);
- logoutUrl.searchParams.set('returnTo', APP_URL);
- headers.set('Location', logoutUrl.toString());
- return new Response(null, { status: 302, headers });
- });
- // --- API Routes (Protected) ---
- // GET Todos
- router.get("/api/todos", async (request, env) => {
- const user = await getUser(request, env);
- if (!user) return new Response("Unauthorized", { status: 401 });
- const todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
- return new Response(JSON.stringify(todos), { headers: { 'Content-Type': 'application/json' } });
- });
- // POST Add Todo
- router.post("/api/todos", async (request, env) => {
- const user = await getUser(request, env);
- if (!user) return new Response("Unauthorized", { status: 401 });
- try {
- const { text } = await request.json();
- if (!text || typeof text !== 'string' || text.trim() === '') {
- return new Response("Invalid todo text", { status: 400 });
- }
- const todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
- const newTodo = { id: crypto.randomUUID(), text: text.trim(), completed: false };
- todos.push(newTodo);
- await env.TODOS_KV.put(user.id + "_todos", JSON.stringify(todos));
- return new Response(JSON.stringify(newTodo), { status: 201, headers: { 'Content-Type': 'application/json' } });
- } catch (e) {
- return new Response("Bad request body", { status: 400 });
- }
- });
- // DELETE Todo
- router.delete("/api/todos/:id", async (request, env) => {
- const user = await getUser(request, env);
- if (!user) return new Response("Unauthorized", { status: 401 });
- const todoId = request.params.id;
- let todos = JSON.parse(await env.TODOS_KV.get(user.id + "_todos")) || [];
- const initialLength = todos.length;
- todos = todos.filter(todo => todo.id !== todoId);
- if (todos.length === initialLength) {
- return new Response("Todo not found", { status: 404 });
- }
- await env.TODOS_KV.put(user.id + "_todos", JSON.stringify(todos));
- return new Response(null, { status: 204 });
- });
- // --- Worker Entry Point ---
- export default {
- async fetch(request, env, ctx) {
- initializeEnv(env); // Make secrets available globally in this request scope
- APP_URL = new URL(request.url).origin; // Ensure APP_URL is set for each request
- return router.handle(request, env, ctx);
- }
- };
- ```
- **Important Security Note on `getUser` function:**
- 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:
- 1. Fetching Auth0's JWKS (JSON Web Key Set) from `https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json`.
- 2. Finding the key in the JWKS that matches the `kid` (Key ID) in the JWT header.
- 3. Using `crypto.subtle.verify()` with the public key to verify the token's signature.
- 4. Validating claims like `iss` (issuer), `aud` (audience), and `exp` (expiration time).
- Cloudflare Workers support `crypto.subtle`, so this is feasible. For brevity in this "simple" example, it's omitted but is critical for security.
- ---
- **Step 4: Deployment**
- 1. **Build (if necessary, though not for this simple setup) and Deploy:**
- ```bash
- wrangler deploy
- ```
- 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".
- 2. **Test:**
- * Open the deployed URL in your browser.
- * You should see the login button.
- * Click login, authenticate with Auth0.
- * You should be redirected back to the app, logged in.
- * Try adding, listing, and deleting todos.
- * Try logging out.
- ---
- This provides a foundational, simple todo app with Auth0 authentication on Cloudflare. For a production system, you'd want to:
- * Implement robust JWT validation.
- * Add more comprehensive error handling.
- * Improve the UI/UX (perhaps with a frontend framework or more advanced vanilla JS).
- * Consider pagination or more advanced KV store usage if lists get very long.
- * Use a proper router library if your app grows.
- * Implement proper CSRF protection for the `/callback` using the `state` parameter by storing it in a short-lived, HttpOnly cookie.
- * 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