Each person gets their own Klippy, named after them. That name must come from the server, from who's signed in, never from the browser: a name the browser chose is a name anyone could change. This lesson adds GitHub sign-in with Remix's own auth, and an allowlist so only you get in.
Verified remix 3.1.0wrangler 4.157.0
Start from d-05 on GitHub. Starting here, or behind? Get the code at that tag:
git clone https://github.com/ryanyogan/klippy cd klippy git checkout d-05
1. The prompt
Fresh session, plan mode:
Goal: add GitHub sign-in so each person gets their own Klippy, chosen on the server from their GitHub id. Use the remix skill. Read node_modules/remix/INDEX.md, guide 10, and the auth, auth-middleware, session and cookie READMEs first. Constraints: - remix/auth's GitHub provider, remix/session with cookie storage, remix/middleware/auth, cop(). No other auth package. - Only my GitHub account id may sign in (ALLOWED_GITHUB_IDS, not logins: those can be renamed); anyone else gets a 403 page and no session. - The agent token stays on the server: add it to the service-binding call, never send it to the browser. - Secrets in .dev.vars and `wrangler secret put` only; placeholders in .dev.vars.example. Touch only apps/web/**. Plan first, then wait for my go-ahead. Done when typecheck and remix doctor pass, and curl shows: / redirects to /login, /auth/github redirects to GitHub, and a cross-site POST /logout gets 403. Report the files and the outputs.
"No other auth package" stops a hand-written OAuth exchange. Auth is where home-made code goes wrong quietly.
The allowlist line. Sign-in says who you are. The allowlist says whether you're allowed. Klippy's notes have no owner column: without the list, anyone with a GitHub account could sign in, spend your model budget and read your notes.
Ids, not logins. A GitHub login can be renamed, and someone else can then register the old name. The numeric id never changes.
2. Find your GitHub id
gh api user --jq .idNo GitHub CLI? Open https://api.github.com/users/<your-login> and read the "id" field. Put it in ALLOWED_GITHUB_IDS in your .dev.vars.
3. What to expect: the parts worth reading
New: app/auth.ts, app/agent.ts, app/actions/auth/, an allowlist with a small Node test, and .dev.vars.example. Changed: routes, router, the home page, the layout and wrangler.jsonc.
The agent's name, chosen on the server:
The token, added on the server side of the binding:
The agent's token check is exactly as it was. The browser never sees the token, and never names the agent.
The allowlist is checked twice: in the callback (no session for a stranger) and again on every request, so removing an id works at once:
4. Check it, up to GitHub's door
With placeholder GitHub credentials in .dev.vars, our checks (real):
scope=read:user: only who you are, not your email or repos.stateandcode_challenge(PKCE) come fromremix/authfor free.The session cookie is signed,
HttpOnlyandSecure.
Also checked: / redirects to /login; a callback with a wrong state gets a 400 and "GitHub sign-in didn't finish"; a POST /logout from another site gets 403 (cop()); one from this site signs you out.
What we didn't record. We haven't created a GitHub OAuth app for the recording, so there's no recorded round trip through GitHub. To try it:
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App. Homepage
http://localhost:8802, callbackhttp://localhost:8802/auth/github/callback. Copy the client id and make a secret.Put
GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRETand aSESSION_SECRET(openssl rand -hex 32) inapps/web/.dev.vars.pnpm dev:web, sign in. Expect a greeting with your name, and your Klippy namedgh-plus your id. A second GitHub account should get the 403 page.
5. Review it
The cookie is
httpOnly,secure,sameSite: 'lax'and signed withSESSION_SECRET.completeAuth()runs before the user is written (a new session id), and logout is a POST that destroys the session.The agent name comes from the session, never a URL or form field.
KLIPPY_TOKENappears only inapp/agent.ts(grep -r KLIPPY_TOKEN apps/web/app).The allowlist compares numeric ids. No real secret in any committed file.
Reject: a hand-written token exchange with
fetch, a session inlocalStorage, an allowlist checked only at sign-in,?token=in a browser URL.
Steer prompts:
You wrote the GitHub token exchange by hand with fetch. Delete it and use createGitHubAuthProvider with startExternalAuth / finishExternalAuth from remix/auth, as the README shows.context.auth.identity doesn't typecheck in home. Move requireUser from the action to the controller's middleware, so the controller's context knows the user is signed in.You compared logins. A login can be renamed and re-registered by someone else: compare the numeric id instead.The second one is a real snag we hit: as action middleware, requireAuth didn't narrow the type; as controller middleware, it did.
Your repo matches tag d-06 (changes from d-05).