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.
npx -y skills add kjuhwa/skills-hub --skill jwt-cookie-auth-on-websocket-upgradeAssembled 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
Bearertokens. - 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
- On
/api/auth/login: check password (rate-limited per IP), sign a JWT withjose:new SignJWT({ sub: 'webui' }).setProtectedHeader({ alg: 'HS256' }) .setIssuedAt(now).setExpirationTime(now + 86400).sign(encoder.encode(SECRET)); - Set cookie:
craft_session=<jwt>; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400; Secure(Secure only when TLS is on; auto-detect viaCRAFT_WEBUI_SECURE_COOKIEenv override). - HTTP requests: parse cookie header,
jwtVerify(token, key, { algorithms: ['HS256'] }), 401 on failure. - WebSocket upgrade: in
WsRpcServeroptions, pass avalidateSessionCookie(cookieHeader): Promise<boolean>:
At upgrade time, the server checks EITHERvalidateSessionCookie: webuiEnabled && serverToken ? async (cookieHeader) => (await validateSession(cookieHeader, serverToken)) !== null : undefinedBearerauth (native clients) OR the cookie (browser clients). - Cookie secret = the same server bearer token (
CRAFT_SERVER_TOKEN) - one secret, two auth mechanisms. - Use
CRAFT_WEBUI_PASSWORDas 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.
Securecookie flag should follow TLS: if listening onws://loopback, omitSecure(browsers reject Secure cookies from non-https). Toggle via env.- Protect
/api/auth/loginwith per-IP rate limiting (e.g. 5/min) - password bruteforce is the obvious attack. - Expose a
CRAFT_WEBUI_WS_URLenv 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=Laxprevents CSRF on login,CORShandles the rest.