Require an API key on /api/* - #13
Open
ashwinimanoj wants to merge 2 commits into
Open
Conversation
Every route under /api/* now needs `Authorization: Bearer <key>`, matched against API_KEY. The HTML pages and /health stay open — the dashboard is the demo, and a browser has nowhere to put a bearer token. Fails closed. With API_KEY unset nothing can match it, so a deployment that never received the variable rejects every request rather than serving the dataset to the internet. The response says only that the request was rejected. Why it was rejected goes on the span as `auth.rejected_reason` — missing, malformed or mismatch — which separates a caller that forgot the header from one guessing keys, without handing that distinction to the caller. Rejections reuse the existing error handler, so they log at warn rather than error and leave the fault rate measuring faults. A rejected request never reaches the router, so nameSpanAfterRoute cannot rename its span: 401s keep the bare `GET`/`POST` name with no http.route. Filtering on auth.rejected_reason finds them instead. Tests run on node:test, added as `npm test`. They drive the real app over HTTP against a Supabase stand-in, which is what proves the gate is mounted in front of the right routes and not merely that the middleware works.
Records the header callers send, the fail-closed behaviour, and the auth.rejected_reason values, so the reason a request was rejected can be found where it actually lives rather than in the response body. States the limit plainly: the HTML pages read the same data the API serves, so the key protects the interface, not the dataset.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Every route under
/api/*now requiresAuthorization: Bearer <key>, matched against a newAPI_KEYenvironment variable./healthand the HTML pages stay open.What changed
src/auth.jsrequireApiKeymiddlewaresrc/app.jsapiRoutestest/auth.test.jstest/helpers/supabase-stub.jspackage.jsonnpm test→node --testREADME.md,.env.exampleBehaviour
Rejected requests get
401and{"error":"Unauthorized"}— nothing more. The reason goes on the span asauth.rejected_reason:missingAuthorizationheadermalformedBearer <token>, or an empty tokenmismatchThat separates a caller that forgot the header from one guessing keys, without telling the caller which it was.
Fails closed. With
API_KEYunset, nothing can match it, so a deployment that never received the variable rejects everything rather than serving the dataset to the internet.Rejections reuse the existing error handler, so they log at
warnrather thanerrorand the fault rate keeps measuring faults.What this does not protect
The HTML pages read the same data the API serves and stay open, because a browser has nowhere to put a bearer token. This is an access gate on automation, not a security boundary around the dataset. Stated plainly in the README rather than left to be discovered.
Known consequence for tracing
A rejected request never reaches Express's router, so
req.routeis unset andnameSpanAfterRoutecannot rename the span. 401s keep the bareGET/POSTname with nohttp.route. Filter onauth.rejected_reasoninstead of by route.Tests
node:test, no new dependencies —enginesalready requires Node 20+.Tests drive the real
createApp()over HTTP rather than mounting the middleware on a throwaway app, which is what proves it is wired in front of the right routes. Verified by mutation: unwiring it fromapp.jsfails 6 tests, weakening the key comparison fails 3.Rollout — order matters
The loadgen in
office-k8s-workloadsalready sendsAuthorization: Bearer, and today's service ignores it, so the caller can be armed before the gate exists:openssl rand -hex 32kubectl create secret generic inventory-service -n observability --from-literal=token=<key>API_KEYin the Vercel projectSteps 4–5 before 2–3 gives a
CrashLoopBackOff: the loadgen's startup call is not retried andexit(1)s on a non-OK response.