Skip to content

feat(auth): add authentication foundation for future Research authoring - #26

Merged
psatomas merged 1 commit into
mainfrom
feature/research-auth-foundation
Sep 1, 2026
Merged

feat(auth): add authentication foundation for future Research authoring#26
psatomas merged 1 commit into
mainfrom
feature/research-auth-foundation

Conversation

@psatomas

@psatomas psatomas commented Sep 1, 2026

Copy link
Copy Markdown
Owner

What

Google sign-in + a server-side authorized-author check, built as an independent foundation for future Research authoring routes to consume. No editor, no /research/write, no draft/preview/publish UI — this PR is purely the auth boundary, proven working, nothing built on top of it yet.

src/lib/research/ and src/app/research/ are completely untouched — confirmed via git diff --stat before committing.

Library

next-auth@4.24.15 — the actual npm latest, not the long-running v5 beta (5.0.0-beta.32, still beta after 32 iterations). Confirmed via research this is the patched release for the July 2026 Auth.js security advisories, not a version predating them. Google provider only, JWT sessions (no database adapter — nothing to persist for one authorized identity), scope explicitly openid email profile only (no Drive/Gmail/Calendar/other API access ever requested).

Architecture

Google → /api/auth/callback/google (library-owned) → JWT session
  → getServerSession() → getAuthorizationResult() → isAuthorizedAuthor()
  → protected boundary (today: auth-check; tomorrow: any authoring route)
  • src/lib/auth/is-authorized-author.ts — the pure comparison (email vs RESEARCH_AUTHOR_EMAIL, requires a non-false email_verified, fails closed if unconfigured). No next-auth import, no I/O — 7 new unit tests.
  • src/lib/auth/authorization.tsgetAuthorizationResult(), the single entry point future authoring code should call. Zero imports from src/lib/research/ — authentication doesn't know a Research repository exists, by design.
  • src/app/api/research/auth-check/route.ts — minimal, unlinked test route: redirect if unauthenticated, 403 if the wrong Google account, 200 if authorized. A Route Handler (not a page) specifically so these map onto real, stable HTTP status codes without needing the experimental forbidden()/authInterrupts flag for a throwaway route.

The one real Cloudflare-specific finding

next-auth v4 itself is fully compatible with this runtime — but its classic pattern of reading process.env.X in a module-scope options object doesn't work here. Confirmed empirically (a debug route, since removed): this project's .dev.vars-sourced secrets are exposed via getCloudflareContext().env, not process.env — only specific OpenNext-managed vars (like NEXTJS_ENV) flow through process.env. Without the fix, the Google provider would have silently gotten an empty clientId/secret and the session an empty encryption key.

Fixed by making getAuthOptions() and getAuthorizationResult() async, resolving config via src/lib/auth/env.ts at request time — the exact same lazy-composition pattern already used by the Oracle service and the Research repository, not a new mechanism. [...nextauth]/route.ts uses next-auth's per-request NextAuth(req, context, options) form for the same reason.

Validation

  • npm run test — 32/32 pass (25 existing + 7 new).
  • npm run lint / npm run build — clean; /api/auth/[...nextauth] and /api/research/auth-check correctly ƒ Dynamic; Research and everything else unchanged.
  • Real workerd (opennextjs-cloudflare preview): anonymous /research//research/[slug] → 200, unauthenticated /api/research/auth-check → 307 to /api/auth/signin, /api/auth/signin renders. Authorization boundary proven with a real, cryptographically valid next-auth session JWT — crafted using the actual AUTH_SECRET the running server resolves, decrypted by the real server code (not mocked): authorized email → 200, different email → 403. Homepage/Projects/Oracle API unaffected; zero new errors in the observability log.
  • Not verifiable from this environment: the live Google OAuth handshake itself (login → consent → callback with a real account) — needs a real Google Cloud OAuth client (only you can create one) and an interactive browser. Everything around it is proven with real cryptography above.

Before this works with a real Google account

See the full report for exact steps, but in short: create a Google Cloud OAuth client (Web application), set authorized redirect URIs to http://localhost:3000/api/auth/callback/google and https://psatomas.com/api/auth/callback/google, then set AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET/AUTH_SECRET/RESEARCH_AUTHOR_EMAIL via wrangler secret put for production (and fill in .dev.vars locally, already gitignored with placeholders).

🤖 Generated with Claude Code

Google sign-in + a server-side authorized-author check, built as an
independent foundation for future Research authoring routes to consume —
no editor, no /research/write, no draft/preview/publish UI. Nothing
under src/lib/research/ or src/app/research/ was touched.

- next-auth@4.24.15 (the actual npm `latest`, not the long-standing v5
  beta) — confirmed via research this is the patched release for the
  July 2026 Auth.js security advisories, not a version predating them.
  Google provider only, JWT sessions (no database adapter — nothing to
  persist for one authorized identity), scope explicitly limited to
  `openid email profile` (no Drive/Gmail/Calendar/other API access).
- src/lib/auth/is-authorized-author.ts: the pure authorization
  comparison (authenticated email vs RESEARCH_AUTHOR_EMAIL, requiring a
  non-false email_verified signal, failing closed if unconfigured) — no
  next-auth import, no I/O, directly unit-tested (7 new tests).
- src/lib/auth/authorization.ts: getAuthorizationResult(), the single
  entry point future authoring code should call. No import from
  src/lib/research/ anywhere in this module — authentication does not
  know a Research repository exists.
- src/lib/auth/config.ts + env.ts: the Auth.js config and its env
  resolution, both async. This is not incidental: confirmed empirically
  that this project's .dev.vars-sourced secrets are exposed via
  getCloudflareContext().env, not process.env, under the actual
  OpenNext/Cloudflare runtime — only specific OpenNext-managed vars
  (like NEXTJS_ENV) flow through process.env. next-auth's classic
  module-scope `process.env.X` options pattern would have silently
  constructed a Google provider with an empty clientId/secret and an
  empty session secret here. The fix mirrors the lazy, per-request
  composition pattern already used by the Oracle service and the
  Research repository, not a new mechanism.
- src/app/api/auth/[...nextauth]/route.ts: the library's standard
  catch-all, using the per-request `NextAuth(req, context, options)`
  form for the same reason above.
- src/app/api/research/auth-check/route.ts: a minimal, unlinked test
  route proving the boundary — redirect if unauthenticated, 403 if
  authenticated as the wrong Google account, 200 if authorized. A Route
  Handler rather than a page specifically so these map onto real HTTP
  status codes with stable APIs, no experimental `forbidden()` /
  authInterrupts flag needed for a throwaway verification route.

Verified under real workerd (opennextjs-cloudflare preview), not just
next build: anonymous /research and /research/[slug] unaffected, the
protected route redirects when unauthenticated, and — using a real,
cryptographically valid next-auth session JWT crafted with the actual
AUTH_SECRET the running server resolves (not mocked) — an authorized
session gets 200 and a different email gets 403. The real Google OAuth
handshake itself is not verifiable from this environment (needs a real
Google Cloud OAuth client and an interactive browser); everything
around it — routing, session decryption, the authorization check
itself — is proven with real cryptography.

No secrets committed. .dev.vars (gitignored) has placeholder
AUTH_GOOGLE_ID/SECRET values and a locally-generated dev-only
AUTH_SECRET; production needs its own values set via `wrangler secret
put` plus a real Google Cloud OAuth client (see implementation report).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@psatomas
psatomas merged commit a86938c into main Sep 1, 2026
1 check passed
@psatomas
psatomas deleted the feature/research-auth-foundation branch September 1, 2026 22:33
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.

1 participant