feat(auth): add authentication foundation for future Research authoring - #26
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/andsrc/app/research/are completely untouched — confirmed viagit diff --statbefore committing.Library
next-auth@4.24.15— the actual npmlatest, 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 explicitlyopenid email profileonly (no Drive/Gmail/Calendar/other API access ever requested).Architecture
src/lib/auth/is-authorized-author.ts— the pure comparison (email vsRESEARCH_AUTHOR_EMAIL, requires a non-falseemail_verified, fails closed if unconfigured). No next-auth import, no I/O — 7 new unit tests.src/lib/auth/authorization.ts—getAuthorizationResult(), the single entry point future authoring code should call. Zero imports fromsrc/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 experimentalforbidden()/authInterruptsflag 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.Xin 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 viagetCloudflareContext().env, notprocess.env— only specific OpenNext-managed vars (likeNEXTJS_ENV) flow throughprocess.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()andgetAuthorizationResult()async, resolving config viasrc/lib/auth/env.tsat request time — the exact same lazy-composition pattern already used by the Oracle service and the Research repository, not a new mechanism.[...nextauth]/route.tsuses next-auth's per-requestNextAuth(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-checkcorrectlyƒDynamic; Research and everything else unchanged.workerd(opennextjs-cloudflare preview): anonymous/research//research/[slug]→ 200, unauthenticated/api/research/auth-check→ 307 to/api/auth/signin,/api/auth/signinrenders. Authorization boundary proven with a real, cryptographically valid next-auth session JWT — crafted using the actualAUTH_SECRETthe 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.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/googleandhttps://psatomas.com/api/auth/callback/google, then setAUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET/AUTH_SECRET/RESEARCH_AUTHOR_EMAILviawrangler secret putfor production (and fill in.dev.varslocally, already gitignored with placeholders).🤖 Generated with Claude Code