Skip to content

feat: ACCESS_CODE site-level authentication (#407)#411

Merged
cosarah merged 12 commits intomainfrom
worktree-access-code
Apr 12, 2026
Merged

feat: ACCESS_CODE site-level authentication (#407)#411
cosarah merged 12 commits intomainfrom
worktree-access-code

Conversation

@wyuc
Copy link
Copy Markdown
Contributor

@wyuc wyuc commented Apr 12, 2026

Summary

  • Add ACCESS_CODE env var that gates access to the entire app
  • When set, unauthenticated users see a full-screen password modal and cannot call APIs
  • When not set, zero behavioral change

How it works

  1. Next.js middleware checks all requests for a valid openmaic_access cookie
  2. First-time visitors see a full-screen modal prompting for the access code
  3. Code is verified server-side via /api/access-code/verify, which sets an HMAC-signed HttpOnly cookie (7-day TTL)
  4. All subsequent requests (including API calls) are validated by verifying the HMAC signature in the cookie
  5. If ACCESS_CODE is not set, middleware exits immediately — zero overhead

New files (10)

File Purpose
middleware.ts Gate all requests, validate HMAC-signed cookie
app/api/access-code/verify/route.ts POST: validate code, set signed cookie
app/api/access-code/status/route.ts GET: return {enabled, authenticated}
components/access-code-guard.tsx Layout wrapper: fetch status, show/hide modal
components/access-code-modal.tsx Full-screen password gate with motion animations

Modified files

  • app/layout.tsx — wrap children with <AccessCodeGuard>
  • .env.example — add ACCESS_CODE
  • lib/i18n/locales/*.json (4 files) — add accessCode.* translation keys (en-US, zh-CN, ja-JP, ru-RU)

Security

  • Cookie value is timestamp.HMAC-SHA256(ACCESS_CODE, timestamp) — never the plaintext code
  • Middleware validates HMAC signature on every request (not just cookie existence)
  • Access code comparison uses constant-time comparison (timingSafeEqual)
  • Middleware uses Web Crypto API for Edge Runtime compatibility
  • HttpOnly + SameSite=Lax cookie
  • Threat model: prevent casual unauthorized access to shared deployments. Not enterprise security.

Code Review Summary

Reviewed by automated code reviewer. All Critical and Important findings addressed:

Severity Issue Resolution
Critical Cookie only checked for existence, not validated HMAC-signed tokens + middleware validation
Critical Timing attack on string comparison timingSafeEqual in verify endpoint
Important Middleware 401 missing errorCode Added to match apiError shape
Important Status endpoint not using apiSuccess Fixed
Important Dead accessCode.submit i18n key Removed from all 4 locales
Suggestion Guard renders null during loading (white flash) Now renders children, modal overlays on top
Suggestion Network error silently disables access control Defaults to enabled: true on error
Fix Node.js crypto not available in Edge Runtime Replaced with Web Crypto API

Test plan

  • Set ACCESS_CODE=test123 in .env.local, start dev server
  • Visit app → full-screen modal appears
  • Enter wrong code → inline error message
  • Enter correct code → modal dismisses, app works
  • Refresh page → no modal (cookie persists)
  • curl /api/verify-model without cookie → 401
  • Remove ACCESS_CODE from env, restart → no modal, all APIs work
  • Verify dark mode renders correctly

Closes #407
Related: #287, Discussion #293, Roadmap #406

🤖 Generated with Claude Code

wyuc and others added 11 commits April 12, 2026 21:04
Returns whether ACCESS_CODE is enabled and whether the current user is
authenticated via cookie, to support the frontend access gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates POST /api/access-code/verify that validates ACCESS_CODE env var
and sets an HttpOnly 7-day cookie on success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Intercepts all requests when ACCESS_CODE env var is set. API routes
return 401 without the openmaic_access cookie; page requests pass
through so the frontend can render the access-code modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add accessCode top-level key to en-US, zh-CN, ja-JP, and ru-RU locale
files with title, placeholder, submit, and error strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps page children with an access-code gate that checks /api/access-code/status on mount and renders AccessCodeModal when authentication is required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the modal dialog component that prompts users for an access code,
using existing Dialog/Button/Input UI components and i18n translation keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix useEffect setState pattern in access-code-guard.tsx
- Format middleware.ts and layout.tsx with Prettier

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace generic Radix Dialog with a custom full-screen gate:
- Mesh gradient background with noise texture
- Frosted glass card with backdrop-blur
- Staggered motion animations on mount
- Inline submit button inside input field
- Success state animation before dismissal
- Proper dark mode support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical:
- Middleware now validates HMAC-signed cookie tokens, not just existence
- Verify endpoint uses timingSafeEqual for constant-time comparison
- Cookie value is now `timestamp.HMAC(ACCESS_CODE, timestamp)` instead of UUID

Important:
- Middleware 401 response includes `errorCode` matching apiError shape
- Status endpoint uses apiSuccess wrapper for consistency
- Remove unused `accessCode.submit` i18n key from all 4 locales

Suggestions:
- Guard renders children during loading (overlay on top, not blocking)
- Network error defaults to enabled=true (safer than silently disabling)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node.js crypto module is not available in Next.js Edge Runtime.
Replace createHmac/timingSafeEqual with crypto.subtle HMAC and
manual constant-length XOR comparison.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@cosarah cosarah left a comment

Choose a reason for hiding this comment

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

LGTM! Clean architecture and solid security fundamentals (HMAC-signed tokens, timingSafeEqual, httpOnly/secure cookies, fail-closed error handling).

Two minor notes for awareness (non-blocking):

  1. Token has no server-side expiry check — the HMAC verification only validates the signature, not the timestamp age. The cookie maxAge handles this in practice, but an extracted token would remain valid indefinitely as long as ACCESS_CODE stays the same.

  2. Duplicate HMAC verification logicverify/route.ts (Node crypto) and middleware.ts (Web Crypto API) implement the same token verification independently. Necessary for Edge Runtime compatibility, but worth keeping in mind if the token format ever changes.

@cosarah cosarah merged commit c071e02 into main Apr 12, 2026
3 checks passed
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.

feat: ACCESS_CODE site-level authentication

2 participants