Repository navigation
[2C-01] Add app-API bearer-token middleware and brain3 token generate CLI - #208
Merged
Merged
Conversation
… setting (#200) Mirrors the brain3-mcp/mcp/auth.py pattern: a Starlette BaseHTTPMiddleware that validates Authorization: Bearer <token> via hmac.compare_digest and returns JSON 401s on missing/bad header. Scoped to /api/app/* by path prefix so MCP and internal clients keep consuming /api/* unauthenticated. Token sourced from settings.APP_BEARER_TOKEN backed by env var BRAIN3_APP_BEARER_TOKEN (validation_alias on the pydantic-settings field) — deliberately distinct from any MCP token per spec v0.9 Q1 + E3. When the env var is unset, install_app_bearer_auth() emits a startup WARNING and skips mounting the middleware so dev work is unblocked without forcing a token in .env. Production deployments must set it before exposing the app API. Adds /api/app/health as the trivial authed probe endpoint the companion app pairing flow calls; closes the [2C-Chunk1] dependency. The existing unauthenticated /health stays as-is for infra probes. Tests cover the four acceptance paths (missing header, wrong token, correct token, env-unset warning) plus a no-regression check that /api/notifications/* continues to answer without an Authorization header even when the middleware is mounted.
scripts/generate_token.py — invokable as python -m scripts.generate_token — prints a fresh secrets.token_urlsafe(48) value to stdout and a one-line operator hint to stderr. Stdout-only-on-success keeps shell capture clean (TOKEN=$(python -m scripts.generate_token)); the stderr hint reminds the operator to set BRAIN3_APP_BEARER_TOKEN in the .env file and restart. No DB write — the token is the shared secret itself. Per-device tokens and rotation tooling are Phase 3. Project does not currently declare [project.scripts] in pyproject.toml (no name/version, not installable as a package), so the spec's optional "brain3-token" entry point is documented as the python -m invocation per the spec's "verify — otherwise document" branch.
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.
Closes #200
Verification
ruff check .— ✅ Pass (All checks passed!)pytest -v— ✅ Pass (1351 passed, 0 failed, 0 skipped — 17.21s)python -m scripts.generate_tokensmoke run: ✅ Pass (token printed to stdout, hint to stderr)Ran locally against develop HEAD at
820312cimmediately before opening this PR.Summary
Adds the bearer-token middleware that gates the new
/api/app/*surface (the companion app + scheduler entry points), plus thebrain3token-generation CLI. Mirrors the pattern inbrain3-mcp/mcp/auth.py(StarletteBaseHTTPMiddleware,hmac.compare_digest, JSON 401 bodies) but scopes auth by path prefix so existing/api/*routes consumed by MCP and internal clients stay unauthenticated. The token lives insettings.APP_BEARER_TOKEN, sourced from env varBRAIN3_APP_BEARER_TOKEN, and is intentionally distinct from any MCP token — separate secrets, separate rotation. Unblocks [2C-05] (FCM dispatch) and the companion-app pairing flow.Changes
app/auth.py(new):AppBearerAuthMiddleware— pass-through for non-/api/app/*paths; on/api/app/*, validatesAuthorization: Bearer <token>with constant-time compare, returns{"error": "Missing bearer token"}(no header) or{"error": "Invalid bearer token"}(wrong secret), both 401. Plusinstall_app_bearer_auth(app, token)helper that mounts the middleware when the token is set or logs a startup warning and skips when it isn't.app/config.py: addsAPP_BEARER_TOKEN: str | Nonefield withvalidation_alias="BRAIN3_APP_BEARER_TOKEN"so the env var stays explicitly prefixed without forcing a global env-prefix on the rest of the settings.populate_by_name=Trueadded tomodel_configto keep field-name access working alongside the alias.app/main.py: importsinstall_app_bearer_auth, calls it afterCORSMiddlewareso the auth gate runs before route dispatch on/api/app/*. Adds the trivial@app.get("/api/app/health")endpoint returning{"ok": True}— the authed probe the companion app hits during URL+token pairing.scripts/generate_token.py(new):python -m scripts.generate_tokenprintssecrets.token_urlsafe(48)to stdout and an operator hint (setBRAIN3_APP_BEARER_TOKEN=<value>in.env, restart) to stderr. AGPL header +__main__block follow thescripts/migrate_to_artifacts.pypattern..env.example: documents the optionalBRAIN3_APP_BEARER_TOKENline so devs know it exists and how to generate a value.tests/test_app_auth.py(new): six tests across the four acceptance paths (missing header, wrong token, correct token, env-unset warning) plus the no-regression check that/api/notifications/*answers without auth even with the middleware mounted, plus an empty-Bearer-value 401.How to Verify
pip install -r requirements.txt -r requirements-dev.txtpytest -v tests/test_app_auth.py— six tests pass.ruff check .— clean.BRAIN3_APP_BEARER_TOKENunset: startuvicorn app.main:app --reloadand confirm the startup log containsBRAIN3_APP_BEARER_TOKEN is not set ….curl http://localhost:8000/api/app/health→ 200{"ok": true}(middleware skipped).curl http://localhost:8000/api/notifications/queue→ 200 (regression intact).TOKEN=$(python -m scripts.generate_token). SetBRAIN3_APP_BEARER_TOKEN=$TOKENin.env, restart the API.curl -i http://localhost:8000/api/app/health→ 401{"error":"Missing bearer token"}.curl -i -H "Authorization: Bearer wrong" http://localhost:8000/api/app/health→ 401{"error":"Invalid bearer token"}.curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/app/health→ 200{"ok":true}.curl -i http://localhost:8000/api/notifications/queue(or any other existing/api/*route) returns its normal response without auth.Deviations
Three small calls worth flagging — none change spec intent, all surface for the record.
generate_token.pywrites the operator hint to stderr, not stdout. The spec's Scope bullet says the script "prints it to stdout with a one-line instruction to setBRAIN3_APP_BEARER_TOKEN=…in the env file"; the Acceptance Criteria says "prints a token to stdout and nothing else on success." Honoring the acceptance criterion (token-only stdout, soTOKEN=$(python -m scripts.generate_token)captures cleanly) means the hint moves to stderr. Standard Unix CLI convention. Both lines of the spec are honored under this reading; flagging because the Scope wording could also be read as "everything to stdout."pyproject.toml[project.scripts]entry. The repo'spyproject.tomlhas no[project]name/version and no[project.scripts]table — it isn't currently installable as a package. The spec's "if the project uses entry points (verify — otherwise documentpython -m scripts.generate_token)" branch applies. Sticking withpython -minvocation; happy to add abrain3-tokenentry point in a follow-up if/when the project becomes installable.CHANGELOG.mdentry in this PR. Recent practice on this branch (Stream F [2F-03] Rule Evaluation Engine #142/[2F-02] Rules — CRUD API Endpoints #141/[2F-04] Default Rules — Seed Data #143, Stream C [2C-26] PR [2C-26] Server: habit + routine completion history endpoints + routine idempotency #207) ships features without per-ticket CHANGELOG updates and batches them into a single docs commit at version cut (the v1.2.04ea98b6commit is the prior pattern). I matched observed convention rather than the once-stated "Update CHANGELOG.md per ticket" line that no longer appears in the BRAIN-canonical org CLAUDE.md v5 or brain3 CLAUDE.md v2. If you'd prefer per-ticket entries restored, easy add — happy to either patch this PR or open a small follow-up that backfills the Stream C tickets shipped so far.Test Results
Acceptance Checklist
python -m scripts.generate_tokenprints a token to stdout and nothing else on success (operator hint goes to stderr).GET /api/app/healthwith correctAuthorization: Bearer <token>→ 200.GET /api/app/healthwithout the header → 401 with body{"error": "Missing bearer token"}.GET /api/app/healthwith the wrong token → 401 with body{"error": "Invalid bearer token"}./api/notifications/*and other existing routes remain unauthenticated — no regression (test_non_app_paths_pass_through_when_middleware_active).tests/test_app_auth.pycovers missing header, wrong token, correct token, env-unset-warning path.