T6: Better Auth login via FIT OAuth #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "t6-better-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
T6: Better Auth login via FIT OAuth
Stacked on #5 (
t5-fit-oauth) → #4 → #3 → #2 → #1. Merge those first.What
src/server/auth.ts(createAuth):usePlural), plus the generic-OAuth plugin for FIT. FIT is a first-class social provider:POST /api/auth/sign-in/social {provider:"fit"}.getToken/getUserInforeuse the T5 client, so we get Basic auth, strict validation and leak-proof errors.check_token, which givesuser_nameand requiresclient_idto match our app, so a token issued to another app is rejected. The display name and e-mail come from Usermap (best effort, falling back tousername@fit.cvut.cz).Single redirect URI:
/auth/callbackforwards to Better Auth's/api/auth/callback/fit. T14 will route MCP logins bystate.Upstream tokens never touch Better Auth's
accountstable:getTokenstashes the token set in a request-scopedAsyncLocalStorage(runAuthRequest).accessToken,refreshTokenandidToken.session.create.before(same request, now with a user id) encrypts the tokens intoconnectionsthrough the T4 token store, and setsusers.usernameserver-side.Without a request scope, tokens are dropped, never stored in plaintext (tested).
Hardening:
scopeson sign-in are rejected, because Better Auth would otherwise merge them into the authorize URL.cvut:grades:course-restrictedare rejected.usernameisinput: false./update-user,/change-email,/delete-userand/link-socialare disabled./login?error=….Optional
ALLOWED_USERS(SPEC §3, for self-hosting): checked before any rows are created and again at session creation. Existing sessions of removed users are revoked onget-session.tanstackStartCookies()is appended in the Worker so session renewal from server-sidegetSessionreaches the browser.UI: a
/loginpage (en/cs) and the home page showing "Signed in as …" with sign-out. The rootbeforeLoadloads{ locale, viewer }in one server call (getRootData).Viewernever contains tokens.Verified
bun run lint && bun run typecheck && bun run test: 92 tests, 13 of them end-to-end login tests against a fake FIT server and Usermap on SQLite. They cover:accountshas null tokens, andconnectionsholds only ciphertext that decrypts to the issued token.bun run dev+ curl:/and/login(en/cs) render. Sign-in returns the FIT authorize URL with the SPEC scopes, S256 andredirect_uri=http://localhost:3000/auth/callback, plus an HttpOnly state cookie./login?error=state_mismatch. An externalcallbackURLgets 403, andscopesin the body gets 400.auth.fit.cvut.cz/login.html. The only step left is a human typing credentials.Astra review (gpt-6-astra), 2 rounds
Fixed:
scopeswere merged into the authorize URL, which allowed requestingcourse-restricted→ rejected in a before hook, plus acheck_tokenscope check.getSessionwere dropped →tanstackStartCookies()plugin.ALLOWED_USERSdidn't affect existing sessions → after hook on/get-sessionrevokes them.Round 2: "No remaining concrete defects", verified against the installed Better Auth source.
Noted, not changed: Better Auth skips the Origin check for cookie-less
sign-in/socialrequests. That's acceptable: the flow is bound to the browser by the HttpOnlybetter-auth.statecookie and the server-side verification record, so a cross-site initiated flow can't be completed by an attacker.Needs you
One browser login at http://localhost:3000 (with
bun run devrunning). It verifies the real flow and answers the open questions indocs/fit-auth.md: whether we get a refresh token, the token lifetime, rotation, and PKCE. It also unblocks T7 (fixtures).🤖 Generated with Claude Code