agentsclimarketplace

Jwt cookie auth on websocket upgrade

Skill kjuhwa/skills-hub/skills/security/jwt-cookie-auth-on-websocket-upgrade

Self-correcting knowledge corpus for Claude Code — 9 stable shape clusters, bias-correction pipeline baked into contribution flow. 47 papers, 45 techniques, 1.1k skills.

Install
npx -y skills add kjuhwa/skills-hub --skill jwt-cookie-auth-on-websocket-upgrade

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Serve a browser-facing web UI that logs in via password, issues an HttpOnly JWT cookie, and then authenticates both subsequent HTTP requests and the WebSocket upgrade from the same cookie — no separate token passing for ws from the browser.

SKILL.md

3.6 KB, 767 tokens by cl100k_base, as published. Nobody here has run it

JWT-cookie auth on WebSocket upgrade

When to use

  • Your server already accepts native-app WebSocket connections with Bearer tokens.
  • You ALSO want to serve a browser web UI where password login is more natural.
  • Don't want the browser to embed a bearer token in JS (xss-reachable) - HttpOnly cookie is safer.

How it works

  1. On /api/auth/login: check password (rate-limited per IP), sign a JWT with jose:
    new SignJWT({ sub: 'webui' }).setProtectedHeader({ alg: 'HS256' })
      .setIssuedAt(now).setExpirationTime(now + 86400).sign(encoder.encode(SECRET));
    
  2. Set cookie: craft_session=<jwt>; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400; Secure (Secure only when TLS is on; auto-detect via CRAFT_WEBUI_SECURE_COOKIE env override).
  3. HTTP requests: parse cookie header, jwtVerify(token, key, { algorithms: ['HS256'] }), 401 on failure.
  4. WebSocket upgrade: in WsRpcServer options, pass a validateSessionCookie(cookieHeader): Promise<boolean>:
    validateSessionCookie: webuiEnabled && serverToken
      ? async (cookieHeader) => (await validateSession(cookieHeader, serverToken)) !== null
      : undefined
    
    At upgrade time, the server checks EITHER Bearer auth (native clients) OR the cookie (browser clients).
  5. Cookie secret = the same server bearer token (CRAFT_SERVER_TOKEN) - one secret, two auth mechanisms.
  6. Use CRAFT_WEBUI_PASSWORD as a shorter password separate from the 32-byte hex server token - users can type it.

Example

import { SignJWT, jwtVerify } from 'jose';
const SECRET = new TextEncoder().encode(serverToken);

async function login(password: string) {
  if (password !== env.CRAFT_WEBUI_PASSWORD && password !== env.CRAFT_SERVER_TOKEN)
    throw new Error('bad pw');
  const now = Math.floor(Date.now() / 1000);
  return new SignJWT({ sub: 'webui' })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt(now).setExpirationTime(now + 86400)
    .sign(SECRET);
}

async function validateSessionCookie(cookieHeader: string | undefined): Promise<boolean> {
  const m = /(?:^|; )craft_session=([^;]+)/.exec(cookieHeader ?? '');
  if (!m) return false;
  try { await jwtVerify(m[1], SECRET, { algorithms: ['HS256'] }); return true; }
  catch { return false; }
}

Gotchas

  • HS256 with a single shared secret is fine for a one-binary server; if you later split auth + RPC, you need asymmetric keys.
  • Secure cookie flag should follow TLS: if listening on ws:// loopback, omit Secure (browsers reject Secure cookies from non-https). Toggle via env.
  • Protect /api/auth/login with per-IP rate limiting (e.g. 5/min) - password bruteforce is the obvious attack.
  • Expose a CRAFT_WEBUI_WS_URL env so reverse-proxied deployments can tell the browser to connect to a DIFFERENT host than the page was served from.
  • Don't reuse the JWT for cross-site requests - SameSite=Lax prevents CSRF on login, CORS handles the rest.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.