@steve31415/auth-client
v1.1.0
Published
Cached client for the Plasticine auth service — Hono middleware plus a lower-level whoami() for WebSocket upgrades and Durable Objects
Downloads
391
Maintainers
Readme
@steve31415/auth-client
Cached client for the Plasticine auth service. Provides a Hono middleware plus a lower-level whoami() function for WebSocket upgrades, Durable Object fetch handlers, and queue consumers.
The library memoizes GET /api/whoami responses in the Cloudflare Cache API — 10 minutes for successful lookups, 30 seconds for 401s — keyed by a SHA-256 hash of the caller's credential. A single caller's steady-state traffic to the auth service drops to ≤6 requests/hour per unique bearer token or session cookie, per colo.
Install
npm install @steve31415/auth-clientYou'll also need hono (peer dependency) and a service binding to the auth worker:
# wrangler.toml
[[services]]
binding = "AUTH"
service = "auth"Hono middleware (recommended)
Drop-in replacement for the hand-rolled auth middleware:
import { Hono } from "hono";
import { createAuthMiddleware } from "@steve31415/auth-client";
type Env = { AUTH: Fetcher; ENVIRONMENT?: string };
const app = new Hono<{
Bindings: Env;
Variables: { user: { user_id: string; email: string } };
}>();
app.use(
"*",
createAuthMiddleware<Env>({
getAuth: (env) => env.AUTH,
domain: "secondthoughts.workers.dev",
}),
);
app.get("/api/items", (c) => {
const user = c.get("user");
return c.json({ email: user.email });
});Behavior:
ENVIRONMENT === "test"→ injects a synthetic user ({ user_id: "test-user", email: "[email protected]" }), skips the AUTH call entirely.- Bearer token from
Authorization: Bearer <token>wins over session cookie if both are present. - 401 on a path starting with
/api/→401 {"error":"Unauthorized"}. - 401 on any other path → redirect to
https://auth.${domain}/login?redirect=<current URL>.
Options
| Option | Type | Default |
|--------|------|---------|
| getAuth | (env) => Fetcher | — (required) |
| domain | string | — (required) |
| testUser | { user_id, email } | { user_id: "test-user", email: "[email protected]" } |
| cacheTtlSeconds | number | 600 |
| negativeCacheTtlSeconds | number | 30 |
| skip | (c: Context) => boolean | — |
skip lets you exempt specific routes (webhooks, health checks, OAuth callbacks, etc.) from auth without splitting the middleware mount points. When the predicate returns true, the middleware passes through without setting c.var.user — handlers must tolerate c.get("user") being undefined.
createAuthMiddleware<Env>({
getAuth: (env) => env.AUTH,
domain: "secondthoughts.workers.dev",
skip: (c) =>
c.req.path.startsWith("/webhooks/") ||
c.req.path === "/robots.txt",
});For more involved customisation (hybrid auth schemes, header-based response decisions, custom failure logging), call whoami() directly from your own middleware instead of trying to configure createAuthMiddleware.
whoami() for non-Hono contexts
For WebSocket upgrades, Durable Object fetch handlers, queue consumers, and crons:
import { whoami } from "@steve31415/auth-client";
async fetch(request: Request): Promise<Response> {
if (request.headers.get("Upgrade") === "websocket") {
const user = await whoami({
auth: this.env.AUTH,
cookie: request.headers.get("cookie") ?? undefined,
ctx: this.ctx,
});
if (!user) return new Response("Unauthorized", { status: 401 });
const pair = new WebSocketPair();
this.ctx.acceptWebSocket(pair[1]);
return new Response(null, { status: 101, webSocket: pair[0] });
}
// ...
}Returns AuthUser on 200, null on 401, throws on transport errors or any other response status.
Pass ctx (ExecutionContext / DurableObjectState) when you have one — cache writes are dispatched via ctx.waitUntil() so they don't delay the response.
How the cache works
- Backing store:
caches.open("auth-client-v1")— a named, versioned Cache API namespace. Thev1suffix is your invalidation lever: bumping it wipes all cached entries across every caller. - Key: SHA-256 of
"<type>:<credential>", where<type>isbearerorsession. Identical credentials used as both a bearer token and a session cookie do not collide. - Session cookie extraction: only the
session=...value is hashed — changes to other cookies (themes, lang, analytics) don't spuriously invalidate the cache. - Stored value: a small JSON envelope (
{status, user?}) withCache-Control: max-age=<ttl>. The Cache API handles expiry. - Scope: Cloudflare's Cache API is per-colo, not global. Each data centre your Worker runs in keeps its own copy. For a 10-min TTL this is usually fine — a caller hitting two colos fetches twice per 10 min instead of once.
Staleness you should know about
A 10-minute positive cache means:
- Revoking a bearer token (
DELETE /api/tokens/:hash) may still authenticate callers for up to ~10 min in each colo that has cached it. - Logging out (
POST /api/logout) may leave a user appearing logged in to other apps for up to ~10 min.
This is acceptable for the vast majority of workloads. Routes that perform sensitive operations (payments, permission changes, account takeover risk) should do an uncached server-side recheck in addition to passing the middleware — do not rely on the cache for authorization-critical decisions.
Migrating from hand-rolled auth middleware
Before:
app.use("*", async (c, next) => {
if (c.env.ENVIRONMENT === "test") {
c.set("user", { email: "[email protected]" });
return next();
}
const res = await c.env.AUTH.fetch("https://auth/api/whoami", {
headers: { Cookie: c.req.header("Cookie") || "" },
});
if (res.ok) {
c.set("user", await res.json());
return next();
}
if (c.req.path.startsWith("/api/")) {
return c.json({ error: "Unauthorized" }, 401);
}
return c.redirect(
`https://auth.secondthoughts.workers.dev/login?redirect=${encodeURIComponent(c.req.url)}`,
);
});After:
app.use(
"*",
createAuthMiddleware<Env>({
getAuth: (env) => env.AUTH,
domain: "secondthoughts.workers.dev",
}),
);The library takes over the test-env bypass, credential detection, API vs page 401 handling, and login redirect construction. The net change per app is ~20 lines → 5.
License
MIT
