Status: proposed
Component: router · middleware · config
Relates to: Admin API feature · future API token auth
Summary
Add a request authentication layer to beacon's HTTP router. The initial implementation uses a single pre-shared API key validated via the Authorization: Bearer <token> header. The middleware is applied selectively — public read endpoints remain unauthenticated, while mutation and admin endpoints require a valid key. This provides the security foundation for the Admin API and any future per-client token system.
Background
Beacon's router (router.New) currently applies no authentication to any endpoint. All routes are publicly accessible to anyone who can reach the server. As beacon gains operational write surfaces (configuration, account management, future API token issuance), an auth boundary becomes necessary.
The simplest viable approach is a single static bearer token loaded from config at startup. This is sufficient for a self-hosted deployment with one operator and can be extended to per-token database-backed auth later without changing the middleware interface.
Design
Configuration
A new AuthConfig struct added to the existing config package:
type AuthConfig struct {
// APIKey is the bearer token required for protected endpoints.
// If empty, protected endpoints are inaccessible (server refuses to start
// or returns 503 — TBD).
APIKey string
}
Loaded from environment (BEACON_API_KEY) or config file alongside existing CORSConfig.
Middleware Signature
// package middleware
// BearerAuth returns an http.Handler that requires a valid bearer token.
// On success it calls next. On failure it returns 401 with a JSON error body.
func BearerAuth(apiKey string, next http.Handler) http.Handler
The middleware:
- Reads the
Authorization header.
- Expects the form
Bearer <token> (case-insensitive scheme).
- Compares the token to
apiKey using subtle.ConstantTimeCompare to avoid timing attacks.
- On mismatch or missing header: responds
401 Unauthorized with {"error": "unauthorized"}.
- On match: calls
next.ServeHTTP.
Route Application
Applied at the sub-router level in router.New, not globally. The pattern:
/api/v1/ → public (read-only node/trace/neighbor data)
/api/v1/admin/ → BearerAuth required
This means existing client integrations reading node or trace data are unaffected. The admin prefix is reserved for the Admin API feature.
Error Response
Consistent JSON error envelope, matching whatever error shape is already used by handlers:
{
"error": "unauthorized"
}
WWW-Authenticate: Bearer response header included per RFC 6750.
Schema Changes
None. Auth is stateless at this layer (single pre-shared key, no DB lookups).
Testing
- Unit tests for the middleware function in
internal/middleware:
- Missing
Authorization header → 401
- Wrong scheme (e.g.
Basic ...) → 401
- Correct scheme, wrong token → 401
- Correct token → passes through to next handler
- Empty
apiKey config → server startup fails (or behavior TBD)
- Integration test:
GET /api/v1/admin/... without token → 401; with valid token → 200
Open Questions
- Startup behavior with no key configured — fail fast (refuse to start) or allow but block all admin routes with 503? Fail-fast is safer.
- Token rotation — out of scope for now; requires restart to change the key. Acceptable for v1.
- HTTPS enforcement — bearer tokens over plain HTTP leak credentials. Should config validation warn or error if TLS is not configured? Worth flagging even if not enforced.
Non-Goals
- Per-user or per-client tokens (deferred to future API token feature)
- JWT or OAuth (unnecessary complexity for a self-hosted single-operator tool)
- Rate limiting (separate concern)
Status:
proposedComponent:
router·middleware·configRelates to: Admin API feature · future API token auth
Summary
Add a request authentication layer to beacon's HTTP router. The initial implementation uses a single pre-shared API key validated via the
Authorization: Bearer <token>header. The middleware is applied selectively — public read endpoints remain unauthenticated, while mutation and admin endpoints require a valid key. This provides the security foundation for the Admin API and any future per-client token system.Background
Beacon's router (
router.New) currently applies no authentication to any endpoint. All routes are publicly accessible to anyone who can reach the server. As beacon gains operational write surfaces (configuration, account management, future API token issuance), an auth boundary becomes necessary.The simplest viable approach is a single static bearer token loaded from config at startup. This is sufficient for a self-hosted deployment with one operator and can be extended to per-token database-backed auth later without changing the middleware interface.
Design
Configuration
A new
AuthConfigstruct added to the existing config package:Loaded from environment (
BEACON_API_KEY) or config file alongside existingCORSConfig.Middleware Signature
The middleware:
Authorizationheader.Bearer <token>(case-insensitive scheme).apiKeyusingsubtle.ConstantTimeCompareto avoid timing attacks.401 Unauthorizedwith{"error": "unauthorized"}.next.ServeHTTP.Route Application
Applied at the sub-router level in
router.New, not globally. The pattern:This means existing client integrations reading node or trace data are unaffected. The admin prefix is reserved for the Admin API feature.
Error Response
Consistent JSON error envelope, matching whatever error shape is already used by handlers:
{ "error": "unauthorized" }WWW-Authenticate: Bearerresponse header included per RFC 6750.Schema Changes
None. Auth is stateless at this layer (single pre-shared key, no DB lookups).
Testing
internal/middleware:Authorizationheader → 401Basic ...) → 401apiKeyconfig → server startup fails (or behavior TBD)GET /api/v1/admin/...without token → 401; with valid token → 200Open Questions
Non-Goals