Skip to content

Authentication #59

Description

@446564

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:

  1. Reads the Authorization header.
  2. Expects the form Bearer <token> (case-insensitive scheme).
  3. Compares the token to apiKey using subtle.ConstantTimeCompare to avoid timing attacks.
  4. On mismatch or missing header: responds 401 Unauthorized with {"error": "unauthorized"}.
  5. 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

  1. Startup behavior with no key configured — fail fast (refuse to start) or allow but block all admin routes with 503? Fail-fast is safer.
  2. Token rotation — out of scope for now; requires restart to change the key. Acceptable for v1.
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requestp3-lowLow priority / nice to have

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions