All /admin routes require the same requireAdmin middleware. Public election, ballot, and results routes do not use this middleware.
Accepted credentials
The middleware checks these paths in order:
| Priority | Mechanism | Accepted request |
|---|---|---|
| 1 | Cloudflare Access | CF-Access-JWT-Assertion or CF-Access-Authenticated-User-Email is present |
| 2 | Admin API key | Authorization: Bearer <key> or Authorization: <key> matches ADMIN_API_KEY |
| 3 | Local bypass | Request hostname is localhost, 127.0.0.1, or ends in .dev |
flowchart LR R[Admin request] --> A{Access header?} A -->|Yes| OK[Continue] A -->|No| K{Valid API key?} K -->|Yes| OK K -->|No| L{Local hostname?} L -->|Yes| OK L -->|No| X[401]
Cloudflare Access trust boundary
Cloudflare Access is expected to authenticate the user at the edge and add its headers before the request reaches the Worker.
Important
The Worker checks only whether either Access header is present. It does not verify the JWT signature, issuer, audience, or expiry. The Cloudflare Access policy and request path are therefore part of the security boundary.
The Worker does not use the Access identity to apply roles or per-admin permissions. Once a request passes requireAdmin, it can reach the selected admin handler.
API key fallback
ADMIN_API_KEY is a Worker secret. Clients send its exact value in the Authorization header:
Authorization: Bearer <ADMIN_API_KEY>The bare-key form is also accepted:
Authorization: <ADMIN_API_KEY>Example:
curl https://vote.ihnyc-rc.org/admin/elections \
-H "Authorization: Bearer $ADMIN_API_KEY"Set the production secret with Wrangler:
wrangler secret put ADMIN_API_KEYFor local Wrangler development, use the gitignored .dev.vars file.
Browser admin pages
The browser UI uses the same middleware as the JSON handlers. Most interactive admin pages embed the API key in the rendered HTML and hold it in a JavaScript variable so they can make further admin requests. Some dashboard handlers still fall back to localStorage.admin_key; the current source does not populate that entry.
When Cloudflare Access authenticates the page request, no API key is passed to the browser.
Failure response
A request that matches none of the accepted paths returns:
HTTP/1.1 401 Unauthorized
Content-Type: application/json{
"error": "Authentication required",
"message": "Use Cloudflare Access (Zero Trust) or provide a valid ADMIN_API_KEY in the Authorization header"
}Sources
Verified against ihnyc-rc-vote commit 2451aa1.
src/middleware/auth.tssrc/index.tssrc/routes/admin.tssrc/routes/admin-ui.ts