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:

PriorityMechanismAccepted request
1Cloudflare AccessCF-Access-JWT-Assertion or CF-Access-Authenticated-User-Email is present
2Admin API keyAuthorization: Bearer <key> or Authorization: <key> matches ADMIN_API_KEY
3Local bypassRequest 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_KEY

For 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.ts
  • src/index.ts
  • src/routes/admin.ts
  • src/routes/admin-ui.ts