Edit on val.town ↗

Auth

There are several ways to add authentication to your Val Town app. Pick the approach that fits your needs, then use the provided prompt to have Townie or your LLM implement it.

Approach Best for Users need
Sign in with Val Town Apps for the Val Town community A Val Town account
HTTP Basic Auth Quick password protection A shared password
Cookie-based login page Personal projects, admin dashboards A shared password
Magic link auth Multi-user apps with email login An email address
Third-party providers Production apps, social login, SSO Depends on provider

The std/oauth middleware lets users sign in with their Val Town account. It handles the full OAuth 2.0 flow, stores sessions in encrypted cookies (no database needed), and gives you access to the user's Val Town profile.

The middleware automatically handles /auth/login, /auth/callback, and /auth/logout routes. Sessions last 30 days.

Prompt for your LLM

Add "Sign in with Val Town" auth to my app using the std/oauth middleware. Import oauthMiddleware and getOAuthUserData from https://esm.town/v/std/oauth/middleware.ts. Wrap the Hono app's fetch handler with oauthMiddleware(app.fetch). Use getOAuthUserData(c.req.raw) to check if a user is logged in and get their profile. Link to /auth/login to sign in and /auth/logout to sign out.

Reference implementation:

import { Hono } from "npm:hono";
import {
  getOAuthUserData,
  oauthMiddleware,
} from "https://esm.town/v/std/oauth/middleware.ts";

const app = new Hono();

app.get("/", async (c) => {
  const session = await getOAuthUserData(c.req.raw);
  if (session?.user) {
    return c.html(
      `<p>Logged in as ${session.user.username}</p>
       <a href="/auth/logout">Log out</a>`,
    );
  }
  return c.html(`<a href="/auth/login">Log in with Val Town</a>`);
});

export default oauthMiddleware(app.fetch);

The simplest way to password-protect your val. The browser shows a built-in login prompt — no UI to build.

Prompt for your LLM

Add HTTP Basic Auth to my val. Check the Authorization header against BASIC_AUTH_USER and BASIC_AUTH_PASS environment variables. Return a 401 with WWW-Authenticate: Basic header if not authenticated.

Store your credentials in environment variables (BASIC_AUTH_USER and BASIC_AUTH_PASS), then check the Authorization header:

export default async function (req: Request): Promise<Response> {
  const auth = req.headers.get("Authorization");

  if (auth) {
    const [, encoded] = auth.split(" ");
    const decoded = atob(encoded);
    const [user, pass] = decoded.split(":");

    if (
      user === Deno.env.get("BASIC_AUTH_USER") &&
      pass === Deno.env.get("BASIC_AUTH_PASS")
    ) {
      return new Response("Welcome! You're authenticated.");
    }
  }

  return new Response("Unauthorized", {
    status: 401,
    headers: { "WWW-Authenticate": 'Basic realm="Protected"' },
  });
}

If you want a custom login form instead of the browser's basic auth popup, you can use a cookie to persist the session. Good for personal projects and admin dashboards.

Prompt for your LLM

Add password auth with a custom login page to my val. Store the password in an APP_PASSWORD environment variable. Show an HTML login form at /login. On correct password, set an HttpOnly session cookie and redirect to /. Add a /logout route that clears the cookie. Check the cookie on all other routes and redirect to /login if not authenticated.

Store your password in an environment variable (APP_PASSWORD). Reference implementation:

const PASSWORD = Deno.env.get("APP_PASSWORD");
const COOKIE_NAME = "session";

export default async function (req: Request): Promise<Response> {
  const url = new URL(req.url);
  const cookies = parseCookies(req.headers.get("Cookie") || "");

  // Handle login form submission
  if (url.pathname === "/login" && req.method === "POST") {
    const form = await req.formData();
    if (form.get("password") === PASSWORD) {
      return new Response(null, {
        status: 302,
        headers: {
          Location: "/",
          "Set-Cookie": `${COOKIE_NAME}=${PASSWORD}; Path=/; HttpOnly; SameSite=Lax`,
        },
      });
    }
    return loginPage("Wrong password.");
  }

  // Handle logout
  if (url.pathname === "/logout") {
    return new Response(null, {
      status: 302,
      headers: {
        Location: "/login",
        "Set-Cookie": `${COOKIE_NAME}=; Path=/; Max-Age=0`,
      },
    });
  }

  // Show login page if not authenticated
  if (cookies[COOKIE_NAME] !== PASSWORD) {
    return loginPage();
  }

  // Authenticated — serve your app
  return new Response(
    "Welcome! You're logged in. <a href='/logout'>Log out</a>",
    {
      headers: { "Content-Type": "text/html" },
    },
  );
}

function loginPage(error?: string) {
  return new Response(
    `<form method="POST" action="/login">
      ${error ? `<p style="color:red">${error}</p>` : ""}
      <input type="password" name="password" placeholder="Password" />
      <button type="submit">Log in</button>
    </form>`,
    { headers: { "Content-Type": "text/html" }, status: 401 },
  );
}

function parseCookies(cookie: string): Record<string, string> {
  return Object.fromEntries(
    cookie.split(";").map((c) =>
      c
        .trim()
        .split("=")
        .map((s) => s.trim()),
    ),
  );
}

For real multi-user auth where each user signs in with their email, remix the Magic Link Starter template. It uses Lucia for session management, Val Town SQLite for storing users and sessions, and Val Town Email for sending magic links.

The flow:

  1. User enters their email on the login page
  2. Your val generates a token and emails them a magic link
  3. User clicks the link, the token is verified, and a session cookie is set
  4. Sessions last 30 days with automatic token rotation

Prompt for your LLM

Remix stevekrouse/luciaMagicLinkStarter and customize it for my app. It uses Lucia for sessions, Val Town SQLite for the database, and Val Town Email for sending magic links.

You can also use any third-party auth provider by connecting to their APIs from your val. Tell your LLM which provider you want to use and it can integrate it.

These older Val Town community libraries still work but are no longer actively recommended. Use std/oauth or the Magic Link Starter instead.