Skip to content

feat(website): replace auth-astro with better-auth - #1191

Merged
fhennig merged 24 commits into
mainfrom
feat/replace-auth-astro-with-better-auth
May 18, 2026
Merged

feat(website): replace auth-astro with better-auth#1191
fhennig merged 24 commits into
mainfrom
feat/replace-auth-astro-with-better-auth

Conversation

@fhennig

@fhennig fhennig commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

resolves #1189, #682, #1188

Summary

  • Replaces auth-astro + @auth/core with better-auth, running in stateless mode (no database required, JWT-based sessions)
  • Removes the custom patch (patches/auth-astro+4.2.0.patch) that was needed to fix TypeScript issues in auth-astro

Changes

  • src/auth.ts (new) — better-auth config with GitHub OAuth provider
  • src/pages/api/auth/[...all].ts (new) — catch-all route handling all auth requests
  • src/middleware/authMiddleware.ts (new) — runs on every request, calls getSession once and populates Astro.locals
  • src/backendApi/backendProxy.ts — reads context.locals.user?.githubId via APIContext
  • src/components/auth/LoginButton.tsxsignIn('github', ...)authClient.signIn.social({ provider: 'github', ... })
  • src/auth/logout.ts — custom @auth/core logout → auth.api.signOut({ headers, asResponse: true })
  • All pages updated to read from Astro.locals instead of calling auth.api.getSession directly
  • astro.config.mjs — removed auth() Astro integration

GitHub user ID handling

better-auth's session.user.id is 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 + mapProfileToUser in auth.ts store the GitHub numeric ID directly on the user object as githubId at login time:

user: {
    additionalFields: {
        githubId: { type: 'string', input: false }
    }
},
socialProviders: {
    github: {
        mapProfileToUser: (profile) => ({ githubId: String(profile.id) })
    }
}

String() is needed because profile.id from 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.githubId is the GitHub numeric ID (as a string) everywhere — no secondary lookups needed. The auth middleware sets it on Astro.locals.user once per request, and all pages and the backend proxy read it from there.

E2E test cookie helper

tests/helpers/auth.ts provides createAuthCookies(accountPayload, sessionPayload) and setupAuthCookie(page, name) to craft valid better-auth session cookies for Playwright tests without a real OAuth flow. The cookies are signed/encrypted using the same AUTH_SECRET the server uses. cookieCache (JWE, 1hr) is enabled so getSession can validate the crafted session_data cookie without an in-memory store lookup.

New Cookies

better-auth sets 3 cookies. A session_token, session_data and account_data. Session data and account data are JWE, encrypted data.

Test plan

  • Sign in with GitHub works end-to-end
  • Logout works
  • Ownership checks work correctly (edit/delete only available for your own collections)
  • Protected pages (/collections, /subscriptions) show the login prompt when logged out
  • E2E tests pass: npm run e2e

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>
@vercel

vercel Bot commented Apr 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboards Ready Ready Preview, Comment May 14, 2026 9:07am

Request Review

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>
fhennig and others added 2 commits May 4, 2026 14:26
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 fengelniederhammer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a few things, but looks good otherwise 👍

Comment thread website/src/auth.ts Outdated
Comment thread website/src/auth.ts Outdated
Comment thread website/tests/helpers/auth.ts Outdated
Comment thread website/tests/helpers/auth.ts Outdated
Comment thread website/tests/helpers/auth.ts Outdated
Comment thread website/src/env.d.ts Outdated
Comment thread website/src/components/auth/LoginButton.tsx
Comment thread website/src/components/auth/UserDropdown.astro Outdated
Comment thread website/src/components/auth/LoginButton.tsx
Comment thread website/src/env.d.ts Outdated
- 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>

@fengelniederhammer fengelniederhammer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Comment thread website/src/env.d.ts
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread website/src/auth.ts
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI: 🔵 nit: Typo — "completly" → "completely".\n\nsuggestion\n // information is in a session cookie; the server is completely stateless.\n

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-auth configuration + catch-all auth API route, and adds an auth middleware that populates Astro.locals.
  • Updates pages/components/backend proxy to read login state and GitHub numeric ID (githubId) from Astro.locals.user.
  • Reworks Playwright E2E auth helper to craft valid better-auth cookies; removes the legacy auth-astro patch 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.

Comment thread website/src/auth.ts
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.
Comment thread website/README.md
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';
@fhennig
fhennig merged commit a2c3e60 into main May 18, 2026
14 checks passed
@fhennig
fhennig deleted the feat/replace-auth-astro-with-better-auth branch May 18, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace auth-astro with better-auth

3 participants