feat(website): replace auth-astro with better-auth - #1191
Conversation
Replaces auth-astro + @auth/core with better-auth, running in stateless mode (no database, JWT-based sessions). Removes the custom patch that was required for auth-astro TypeScript compatibility. - Add better-auth config (src/auth.ts) with GitHub OAuth and trustedProxyHeaders - Add catch-all API route at /api/auth/[...all] to handle auth requests - Replace getSession() calls across all pages and backendProxy with auth.api.getSession() - Replace signIn() in LoginButton with authClient.signIn.social() - Replace custom logout implementation with auth.api.signOut() - Update E2E test helper to use better-auth cookie name and JWT format - Remove auth-astro Astro integration from astro.config.mjs - Remove patches/auth-astro+4.2.0.patch and patch-package NOTE: session.user.id must be verified to contain the GitHub numeric user ID after a real login — see TODO in src/auth.ts. The E2E test token format is also a best-guess pending verification — see TODO in tests/helpers/auth.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
auth.api.signOut() returns typed data, not a Response object, so calling .headers.getSetCookie() on it threw a 500. The asResponse option makes it return a proper Response with Set-Cookie headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
better-auth's session.user.id is a randomly generated internal ID, not the GitHub numeric ID the backend uses for ownership checks. Adds a getGitHubUserId() helper that retrieves the correct ID via auth.api.listUserAccounts() and updates backendProxy and the two collection pages that do ownership comparisons. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s user field Replaces per-page auth calls with a single middleware that runs once per request and populates Astro.locals. The GitHub numeric user ID is now stored as a dedicated `githubId` additionalField on the user object via `mapProfileToUser`, so it is available directly on `session.user` without any secondary lookups. - Add `authMiddleware` that calls `getSession` once and sets `context.locals.user` and `context.locals.session` - Configure `user.additionalFields.githubId` + `mapProfileToUser` in `auth.ts` to populate it from the GitHub profile at login time - Remove `getGitHubUserId` helper (no longer needed) - Update `backendProxy` to read `context.locals.user?.githubId` via `APIContext` instead of re-deriving the ID from headers - Update all pages/components to read from `Astro.locals` instead of calling `auth.api.getSession` directly - Update `App.Locals` types to use the inferred auth user type so `githubId` is typed correctly everywhere - Remove unused `jose` devDependency and E2E test auth helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds tests/helpers/auth.ts with createAuthCookies() and setupAuthCookie() that craft valid better-auth session cookies for Playwright tests without needing a real OAuth flow. Also sets cookiePrefix to 'gen-spectrum' in auth config (required for the helper to match what the server sets). Note: E2E cookie injection is untested — cookieCache also needs to be enabled in auth.ts before getSession will accept the crafted session_data cookie without a store lookup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The patches/ directory was deleted when auth-astro was replaced with better-auth, causing the Docker build to fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enables better-auth cookieCache (JWE, 1hr) so getSession can validate crafted session cookies without an in-memory store lookup. This makes the E2E auth helper work across the separate test/server processes. Also aligns the E2E test GitHub ID with the seeded collection's userId so ownership checks pass on the edit page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
profile.id from GitHub is a number but githubId is declared as string. Without String(), the stored value is a number and ownership checks (currentUserId === collection.ownedBy) fail due to type mismatch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The type says string but GitHub returns a number at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fengelniederhammer
left a comment
There was a problem hiding this comment.
Found a few things, but looks good otherwise 👍
- Set 7-day session expiry explicitly - Add refreshCache: true so stateless JWE cookie stays fresh - Enable storeAccountCookie for fully stateless operation - Fix cookie prefix from 'gen-spectrum' to 'genspectrum' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also fix cookie prefix to match auth.ts ('genspectrum').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… to user Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion YAML Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| type AuthUser = NonNullable<Awaited<ReturnType<(typeof import('./auth').auth)['api']['getSession']>>>['user']; | ||
|
|
||
| interface Locals { | ||
| user: AuthUser | null; // we use our own user type, see not above |
There was a problem hiding this comment.
AI: 🔵 nit: Typo — "see not above" → "see note above".\n\nsuggestion\n user: AuthUser | null; // we use our own user type, see note above\n
| session: { | ||
| expiresIn: 60 * 60 * 24 * 7, // 7 days | ||
| // The cookie cache in combination with refreshCache: true, means that all the session | ||
| // information is in a session cookie; the server is completly stateless. |
There was a problem hiding this comment.
AI: 🔵 nit: Typo — "completly" → "completely".\n\nsuggestion\n // information is in a session cookie; the server is completely stateless.\n
There was a problem hiding this comment.
Pull request overview
Migrates the Astro website authentication stack from auth-astro/@auth/core to better-auth (stateless/JWT cookie cache), centralizing session resolution in middleware and updating UI/API code to consume Astro.locals.
Changes:
- Introduces
better-authconfiguration + catch-all auth API route, and adds an auth middleware that populatesAstro.locals. - Updates pages/components/backend proxy to read login state and GitHub numeric ID (
githubId) fromAstro.locals.user. - Reworks Playwright E2E auth helper to craft valid
better-authcookies; removes the legacyauth-astropatch and integration wiring.
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| website/tests/helpers/auth.ts | Replaces AuthJS JWT cookie helper with better-auth cookie crafting utilities for E2E. |
| website/tests/collections/collectionForm.spec.ts | Aligns test user ID with E2E_GITHUB_ID. |
| website/src/pages/subscriptions/index.astro | Switches login check to Astro.locals.user. |
| website/src/pages/subscriptions/create.astro | Switches login check to Astro.locals.user. |
| website/src/pages/logout.astro | Uses better-auth sign-out response cookies via Set-Cookie headers. |
| website/src/pages/collections/[organism]/index.astro | Switches login check to Astro.locals.user. |
| website/src/pages/collections/[organism]/create.astro | Switches login check to Astro.locals.user. |
| website/src/pages/collections/[organism]/[id]/index.astro | Uses Astro.locals.user?.githubId for ownership/user context. |
| website/src/pages/collections/[organism]/[id]/edit.astro | Uses Astro.locals.user?.githubId for ownership checks and gating. |
| website/src/pages/api/auth/[...all].ts | Adds catch-all route delegating to auth.handler. |
| website/src/middleware/authMiddleware.ts | Adds middleware to resolve session once per request and populate locals. |
| website/src/middleware.ts | Wires auth middleware into global middleware sequence. |
| website/src/layouts/base/header/HamburgerMenu.astro | Uses Astro.locals.user for logged-in UI and username display. |
| website/src/env.d.ts | Defines App.Locals and a derived App.AuthUser type for locals.user. |
| website/src/config.ts | Adds trustedOrigins to config schema + accessor for better-auth. |
| website/src/components/auth/UserDropdown.astro | Changes props to accept user instead of session. |
| website/src/components/auth/LoginState.astro | Uses Astro.locals.user and passes user to dropdown. |
| website/src/components/auth/LoginButton.tsx | Replaces auth-astro client signIn with better-auth client social sign-in. |
| website/src/backendApi/backendProxy.ts | Reads context.locals.user?.githubId instead of calling getSession. |
| website/src/auth/logout.ts | Replaces custom AuthJS signout request with auth.api.signOut. |
| website/src/auth.ts | Adds better-auth configuration including githubId mapping + cookie cache. |
| website/src/auth.config.mjs | Removes legacy auth-astro config file. |
| website/README.md | Documents AUTH_SECRET env var for better-auth. |
| website/patches/auth-astro+4.2.0.patch | Removes no-longer-needed patch-package patch for auth-astro. |
| website/package.json | Removes auth-astro/@auth/core, adds better-auth, removes postinstall patch step. |
| website/package-lock.json | Lockfile updates reflecting dependency migration. |
| website/astro.config.mjs | Removes auth-astro integration. |
| Dockerfile_website | Stops copying patches; continues copying backend config resources to /config. |
| backend/src/main/resources/application-dashboards-staging.yaml | Adds dashboards.auth.trustedOrigins for staging. |
| backend/src/main/resources/application-dashboards-prod.yaml | Adds dashboards.auth.trustedOrigins for prod. |
Files not reviewed (1)
- website/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| session: { | ||
| expiresIn: 60 * 60 * 24 * 7, // 7 days | ||
| // The cookie cache in combination with refreshCache: true, means that all the session | ||
| // information is in a session cookie; the server is completly stateless. |
| This value is not set in config files, as it usually depends on the service name of the backend. | ||
| - `GITHUB_CLIENT_SECRET`: The client secret of the GitHub app. | ||
| This value is supposed to be kept secret and should not be stored in the configuration files. | ||
| - `AUTH_SECRET`: The secret used by `better-auth` for encryption, signing and hashing if cookies/sessions. |
| clientId: "Iv23liLcTmoYOEje50sW" | ||
| trustedOrigins: | ||
| - "https://genspectrum.org" | ||
| - "http://localhost:4321" |
| clientId: "Iv23li4tJm7xQ3VkM3OC" | ||
| trustedOrigins: | ||
| - "https://staging.genspectrum.org" | ||
| - "http://localhost:4321" |
| throw new Error('AUTH_SECRET environment variable is not set'); | ||
| } | ||
| const SECRET: string = rawSecret; | ||
| const COOKIE_PREFIX = 'genspectrum'; |
resolves #1189, #682, #1188
Summary
auth-astro+@auth/corewithbetter-auth, running in stateless mode (no database required, JWT-based sessions)patches/auth-astro+4.2.0.patch) that was needed to fix TypeScript issues in auth-astroChanges
src/auth.ts(new) — better-auth config with GitHub OAuth providersrc/pages/api/auth/[...all].ts(new) — catch-all route handling all auth requestssrc/middleware/authMiddleware.ts(new) — runs on every request, callsgetSessiononce and populatesAstro.localssrc/backendApi/backendProxy.ts— readscontext.locals.user?.githubIdviaAPIContextsrc/components/auth/LoginButton.tsx—signIn('github', ...)→authClient.signIn.social({ provider: 'github', ... })src/auth/logout.ts— custom@auth/corelogout →auth.api.signOut({ headers, asResponse: true })Astro.localsinstead of callingauth.api.getSessiondirectlyastro.config.mjs— removedauth()Astro integrationGitHub user ID handling
better-auth's
session.user.idis a randomly generated internal ID, not the GitHub numeric user ID. The backend uses the GitHub numeric ID for ownership checks (e.g. who can edit a collection).The solution:
user.additionalFields+mapProfileToUserinauth.tsstore the GitHub numeric ID directly on the user object asgithubIdat login time:String()is needed becauseprofile.idfrom GitHub is a number, but the backend stores and compares user IDs as strings. Without the coercion, ownership checks fail due to type mismatch.This means
session.user.githubIdis the GitHub numeric ID (as a string) everywhere — no secondary lookups needed. The auth middleware sets it onAstro.locals.useronce per request, and all pages and the backend proxy read it from there.E2E test cookie helper
tests/helpers/auth.tsprovidescreateAuthCookies(accountPayload, sessionPayload)andsetupAuthCookie(page, name)to craft valid better-auth session cookies for Playwright tests without a real OAuth flow. The cookies are signed/encrypted using the sameAUTH_SECRETthe server uses.cookieCache(JWE, 1hr) is enabled sogetSessioncan validate the craftedsession_datacookie without an in-memory store lookup.New Cookies
better-authsets 3 cookies. Asession_token,session_dataandaccount_data. Session data and account data are JWE, encrypted data.Test plan
/collections,/subscriptions) show the login prompt when logged outnpm run e2e