Files
OpenMAIC/middleware.ts
T
wyucandClaude Opus 4.8 ee79762ef9 fix(access-code): expire tokens server-side and throttle verification behind a trusted proxy (#1513)
When ACCESS_CODE is set, verification tokens (timestamp.HMAC) never expired
because neither verifier checked the timestamp, and POST /api/access-code/verify
had no attempt throttling.

- Enforce a 7-day token lifetime in a shared, Edge-safe module used by both the
  Node verifier and the middleware Web-Crypto verifier, and reject non-canonical
  signatures in both.
- Rate-limit verification only when the client identity is trusted
  (TRUST_PROXY_HEADERS=true): a per-client sliding window (10 failures / 60s)
  whose attempt is reserved atomically at check time, returning 429 with
  Retry-After. Without a trusted proxy the app cannot attribute requests to a
  client, so no shared throttle is applied — a long random ACCESS_CODE is the
  protection, and a warning is logged when it is short.
- Store bounded, copied identity keys so a large forwarding header cannot retain
  memory.
- Document the behavior in .env.example, README, and configuration docs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 17:47:14 +08:00

51 lines
1.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { isAgentRuntimeConfigured, isProWorkbenchEnabled } from '@/lib/config/feature-flags';
import { verifyAccessTokenEdge } from '@/lib/server/access-token-edge';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Return an actual server-side 404 when either half of the workbench is off.
// Edge middleware cannot reliably inspect server-only deployment variables,
// so it enforces the public gate and leaves the complete runtime/database
// check to Node. A Node-hosted middleware uses the same gate as startup.
const canInspectServerRuntime = process.env.NEXT_RUNTIME !== 'edge';
const workbenchEnabled =
isProWorkbenchEnabled() && (!canInspectServerRuntime || isAgentRuntimeConfigured());
if (!workbenchEnabled && (pathname === '/workbench' || pathname.startsWith('/workbench/'))) {
return new NextResponse('Not found', { status: 404 });
}
const accessCode = process.env.ACCESS_CODE;
if (!accessCode) {
return NextResponse.next();
}
// Whitelist: access-code endpoints, health check
if (pathname.startsWith('/api/access-code/') || pathname === '/api/health') {
return NextResponse.next();
}
// Check cookie — validate HMAC signature, not just existence
const cookie = request.cookies.get('openmaic_access');
if (cookie?.value && (await verifyAccessTokenEdge(cookie.value, accessCode))) {
return NextResponse.next();
}
// API requests without valid cookie → 401
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{ success: false, errorCode: 'INVALID_REQUEST', error: 'Access code required' },
{ status: 401 },
);
}
// Page requests → let through, frontend shows modal
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|logos/).*)'],
};