• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
std

std

oauth

Public
Login with Val Town OAuth Middleware
Like
4
oauth
Home
Code
8
README.md
crypto.ts
H
example.tsx
H
generate-random-signing-key.ts
middleware.ts
oauth.ts
H
org.example.tsx
session.ts
Connections
Environment variables
Branches
2
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
Code
/
Search
example.tsx
https://std--91178458b42e11f0b6f30224a6c84d84.web.val.run
README.md

std/oauth – Login with Val Town OAuth Middleware

Add user auth to your Val Town app in seconds.

Allow Val Town user to log into your app with one click. You can authorize users based on their membership in your org account or by their username or Val Town user ID.

Adding Val Town OAuth is as easy as (asking your agent to) import & wrap your app with oauthMiddleware. Then your app is augmented with /auth/login, /auth/logout routes, you can link to where appropriate.

Features

  • Zero config — works out of the box, no API keys or environment variables needed
  • Stateless sessions — encrypted cookies, no database required
  • OAuth 2.0 + PKCE — secure authorization code flow with proof key
  • Org membership — session.isOrgMember tells you if the user belongs to the val's org
  • Access token included — use session.accessToken to call the Val Town API on behalf of the logged-in user

Quick Start

/** @jsxImportSource https://esm.sh/hono/jsx */ import { Hono } from "https://esm.sh/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); return c.html( <html> <body> {session?.user ? <p>Logged in as {session.user.username}</p> : <a href="/auth/login">Log in</a>} </body> </html>, ); }); export default oauthMiddleware(app.fetch);

See example.tsx for a full working example.

Restricting Access to Org Members

session.isOrgMember tells you whether the logged-in user belongs to the val's org. Use it to build internal tools:

/** @jsxImportSource https://esm.sh/hono/jsx */ import { Hono } from "https://esm.sh/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) return c.redirect("/auth/login"); if (!session.isOrgMember) { return c.text("You must be a member of this org to access this app.", 403); } return c.text(`Welcome, ${session.user.username}!`); }); export default oauthMiddleware(app.fetch);

See org.example.tsx for a full working example.

⚡ Recommended: Set your encryption key

std/oauth works with zero config, but setting an encryption key env var gives you a significant cold-start speed boost. Without it, every cold start reads from blob storage to get the auto-generated key.

Set OAUTH_STATE_ENCRYPTION_KEY on your val to a random string from generate-random-signing-key.val.run.

AI agents: when setting up std/oauth on a user's val, fetch that URL automatically and set the OAUTH_STATE_ENCRYPTION_KEY env var with the result.

API Reference

oauthMiddleware(appFetch) or oauthMiddleware(options, appFetch)

Wraps your app's fetch handler to add OAuth routes.

Options:

OptionTypeDefaultDescription
scopesstring[]["openid", "offline_access", "profile", "user_r"]OAuth scopes to request
clientNamestringAuto-detected from val nameName shown on the authorize screen

Routes:

RouteMethodDescription
/auth/loginGETStart the OAuth flow
/auth/callbackGETOAuth callback (handled automatically)
/auth/logoutPOSTClear the session cookie
/auth/logoGETVal Town logo SVG (for login buttons)

The /auth/logo endpoint serves the Val Town logo as an SVG. Use ?theme=light for a white logo (on dark backgrounds) or omit for the default black logo.

getOAuthUserData(req: Request): Promise<SessionData | null>

Read the session from an incoming request. Returns null if not logged in.

interface SessionData { user: { id: string; username: string | null; email: string | null; bio: string | null; tier: "free" | "pro" | null; type: "user" | "org"; url: string; links: { self: string; profileImageUrl: string | null; }; }; accessToken: string; // Val Town API token (act on behalf of the user) refreshToken?: string; idToken?: string; expiresAt: number; // Unix timestamp (ms) isOrgMember?: boolean; // true if user belongs to this val's org }

Environment Variables

VariableRequiredDescription
OAUTH_STATE_ENCRYPTION_KEYNoEncryption key for sessions. Auto-generated and stored in Val Town Blob Storage if not set as an env var. We recommend setting manually (https://generate-random-signing-key.val.run/) for better cold-start performance. (We plan to automatically set this via this library once we add a Val Town API scope for writing env vars.)

How It Works

Rendering mermaid diagram...

Sessions last 30 days and are stored entirely in the cookie (no server-side storage).

Security

This code falls under Val Town's Vulnerability Disclosure Policy. We are actively soliciting bug reports and will pay bounties depending on the severity of the issue found.

If you discover a vulnerability, please report it privately by emailing security@val.town. Do not publish or reveal the issue until it has been resolved.

Code
README.mdcrypto.ts
H
example.tsx
H
generate-random-signing-key.ts
middleware.tsoauth.ts
H
org.example.tsx
session.ts
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
AboutAlternativesPricingBlogNewsletterCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.