Standardize cron / Bearer auth across API routes
Nobody has claimed this yet.
- Dominant language
- HTML
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Problem
The codebase has two different patterns for Bearer-token-protected API routes:
- The shared helper
requireBearerSecret(request, ENV_NAME)(and its cron-specific wrapperrequireCronSecret) used by every/api/sync/*route. - A custom local
validateAuth(request)in/api/invoices/ingest/route.tsthat re-implements the same pattern slightly differently.
Two implementations of the same primitive means a future hardening (e.g. constant-time comparison, audit logging on auth failure, rate limiting) has to be applied twice and can drift. It's also harder for a new contributor — or an AI agent — to discover the canonical way to add a new protected route.
Evidence
src/app/api/invoices/ingest/route.ts:21-26:
function validateAuth(request: NextRequest): boolean {
const secret = process.env.INVOICE_INGEST_SECRET;
if (!secret) return false;
const authHeader = request.headers.get(\"authorization\");
return authHeader === `Bearer ${secret}`;
}
vs the shared helper used by /api/sync/* routes (see src/lib/cron-auth.ts or wherever requireBearerSecret lives).
The local one:
- Does plain string equality (timing-attack-vulnerable; the shared helper should use
timingSafeEqualif it doesn't already). - Returns a
booleaninstead of returning aNextResponse | nulllike the shared helper, so the calling code has to write its own 401 response.
Proposed approach
- Read
src/lib/cron-auth.ts(or the canonical location ofrequireBearerSecret) to confirm the contract. - If the shared helper does not already use a constant-time comparison, fix that first as part of this issue (use
crypto.timingSafeEqualafter coercing toBufferof equal length). - Replace
validateAuthin/api/invoices/ingest/route.tswith a call to the shared helper passing\"INVOICE_INGEST_SECRET\". - Sweep the rest of
src/app/api/**/route.tsfor any other one-off Bearer checks and replace them. - Add a unit test for
requireBearerSecret:- missing header → returns 401 response
- wrong secret → returns 401
- correct secret → returns null (i.e. "continue")
- missing env var → returns 500 / clear error (or whatever the helper currently does — match it)
Acceptance criteria
- No route-local Bearer-validation helpers remain in
src/app/api/. -
requireBearerSecretuses constant-time comparison. - Behavior of
/api/invoices/ingestis unchanged (auth still works, error responses still match). - Unit tests cover the helper.
-
pnpm lint && pnpm typecheck && pnpm testpass.
Verification
- POST
/api/invoices/ingestwith noAuthorizationheader → 401. - POST with wrong Bearer → 401.
- POST with correct Bearer + valid PDF → 200 (or expected response).
- Existing
/api/sync/*routes still work unchanged.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading src/lib/cron-auth.ts and src/app/api/invoices/ingest/route.ts to confirm the shared helper contract, then sweep src/app/api/**/route.ts for other local Bearer checks. Add the specified helper tests for missing, wrong, and correct credentials, and verify with pnpm lint && pnpm typecheck && pnpm test plus the listed ingest and sync route checks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- api, authentication, security, testing
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100