Add a development-only login bypass #15

Closed
opened 2026-07-28 04:52:54 +00:00 by claude-bot · 1 comment

Context

The README states plainly: "Without OAuth credentials configured the app starts but the
login button won't complete (for development you can directly create a User in the DB
or add a dev-login bypass later)." Hand-editing the database to get a session is a poor
developer experience and blocks automated end-to-end testing entirely.

Scope

An explicit, development-only login path that issues a normal session for a chosen
user and role.

Implementation notes

  • Gate on a dedicated flag (CIRCA_DEV_LOGIN_ENABLED) that defaults to off and is
    rejected outright when CIRCA_ENV=production — the fail-fast config issue in this
    milestone should treat it as a hard startup error, not a warning. A dev bypass that can
    be switched on in production is a full authentication bypass.
  • When enabled, expose a route that creates or reuses a user with a specified email and
    role, and issues the same signed session cookie the OAuth callback would. Reusing the
    real session path is the point — a parallel session mechanism would drift from the
    code that matters.
  • Surface a clearly labelled dev-login control on the login page, visible only when the
    backend reports the bypass is active.
  • Log every dev-login use at warning level with the impersonated identity.
  • Document it in the README as development-only, replacing the current
    "create a User in the DB" advice.

Done when

  • A developer can sign in locally with no OAuth credentials configured
  • The bypass is off by default and refuses to start under CIRCA_ENV=production
  • Sessions issued by the bypass are indistinguishable downstream from OAuth sessions
  • Role can be chosen so reviewer and admin paths are both testable
  • The README no longer advises editing the database by hand

References

  • README.md (current workaround)
  • backend/app/auth/, frontend/src/pages/Login

Related: the fail-fast configuration issue. Blocks the end-to-end reviewer flow test.

## Context The README states plainly: "Without OAuth credentials configured the app starts but the login button won't complete (for development you can directly create a User in the DB or add a dev-login bypass later)." Hand-editing the database to get a session is a poor developer experience and blocks automated end-to-end testing entirely. ## Scope An explicit, development-only login path that issues a normal session for a chosen user and role. ## Implementation notes - Gate on a dedicated flag (`CIRCA_DEV_LOGIN_ENABLED`) that defaults to off and is **rejected outright** when `CIRCA_ENV=production` — the fail-fast config issue in this milestone should treat it as a hard startup error, not a warning. A dev bypass that can be switched on in production is a full authentication bypass. - When enabled, expose a route that creates or reuses a user with a specified email and role, and issues the same signed session cookie the OAuth callback would. Reusing the real session path is the point — a parallel session mechanism would drift from the code that matters. - Surface a clearly labelled dev-login control on the login page, visible only when the backend reports the bypass is active. - Log every dev-login use at warning level with the impersonated identity. - Document it in the README as development-only, replacing the current "create a User in the DB" advice. ## Done when - [ ] A developer can sign in locally with no OAuth credentials configured - [ ] The bypass is off by default and refuses to start under `CIRCA_ENV=production` - [ ] Sessions issued by the bypass are indistinguishable downstream from OAuth sessions - [ ] Role can be chosen so reviewer and admin paths are both testable - [ ] The README no longer advises editing the database by hand ## References - `README.md` (current workaround) - `backend/app/auth/`, `frontend/src/pages/Login` Related: the fail-fast configuration issue. Blocks the end-to-end reviewer flow test.
claude-bot added this to the v0.2.0 milestone 2026-07-28 04:52:54 +00:00
Author

Done in afd5e5c. backend/tests/test_dev_login.py (24) and frontend/src/pages/LoginPage.test.tsx (8).

Three guards, each asserted separately

POST /api/auth/dev-login issues a real session for a chosen email and role. Said plainly that is an authentication bypass, so the tests that matter are not the ones proving it works — they are the ones proving it cannot be reached. Each guard is checked on its own, because a defence that holds only while the other two do is one refactor from being no defence at all:

  1. Off by default.
  2. validate_settings refuses to start production with it on (#14).
  3. The route re-checks the flag and the environment on every request. This is the one the issue didn't ask for and I'd argue matters most: validate_settings runs when the application is built. An app built in development whose settings are later changed — a reload, a long-lived process, a test — would otherwise keep serving a route that mints admin sessions. "Unreachable" is a property of today's wiring; this isn't.

Disabled, it answers 404, not 403: a deployment without this feature should not advertise that the feature exists.

Reusing the real session path

As the issue asks, and it is the point. Same SessionRepository.create, same create_session_cookie, same signer — so revocation (#61), the sliding idle window and the absolute ceiling all apply, and #12's journey will exercise what a reviewer actually holds rather than a parallel mechanism that drifts from it. Asserted directly: the cookie decodes to a real user_session row, logout ends it, a viewer session is refused a reviewer route the same way, and what it writes is attributed to it.

Two details worth naming:

  • The account is namespaced as dev-login:<email> in provider_sub. That column is the join key to the identity provider; a bypass account sharing the namespace could collide with a real one, or be mistaken for one later by someone auditing who had access.
  • Re-signing-in switches the role on the same account rather than creating a second user, so a test can assert "the same person cannot do this" — which is most of what a role matrix is for.

Every use logs at warning level with the identity and role.

Frontend

The sign-in page grows a loudly labelled panel when the backend reports the bypass is on. A failed probe is treated as off — defaulting the other way would put an authentication-bypass form on the one page an unauthenticated stranger can always reach. The status endpoint discloses exactly one boolean, and that is asserted so it does not grow into a description of how the deployment is configured.

A real defect fell out

Writing the "it logs its use" assertion found that alembic/env.py was switching off the whole application's logging. It called fileConfig with the generated template's default disable_existing_loggers=True, which sets .disabled on every logger that already exists — permanently, with no error and nothing to notice.

The symptom was a test that passed alone and failed in a full run: alone, the route's module hadn't been imported when the migration ran, so there was nothing to disable. It reaches production through app/cli/, whose tools migrate and then work in the same process, and it would have quietly undermined #89. test_harness_isolation.py now asserts both the flag and that a record actually reaches a handler afterwards.

Done when

  • A developer can sign in locally with no OAuth credentials configured
  • The bypass is off by default and refuses to start under production
  • Sessions issued by the bypass are indistinguishable downstream from OAuth sessions
  • Role can be chosen so reviewer and admin paths are both testable
  • The README no longer advises editing the database by hand

1020 passed, 8 skipped backend; 58 frontend; ruff and tsc clean.

Note on naming: the issue says CIRCA_ENV, but the existing setting is CIRCA_ENVIRONMENT and that is what both this and #14 use.

Done in afd5e5c. `backend/tests/test_dev_login.py` (24) and `frontend/src/pages/LoginPage.test.tsx` (8). ## Three guards, each asserted separately `POST /api/auth/dev-login` issues a real session for a chosen email and role. Said plainly that is an authentication bypass, so the tests that matter are not the ones proving it works — they are the ones proving it cannot be reached. Each guard is checked on its own, because a defence that holds only while the other two do is one refactor from being no defence at all: 1. **Off by default.** 2. **`validate_settings` refuses to start production with it on** (#14). 3. **The route re-checks the flag *and* the environment on every request.** This is the one the issue didn't ask for and I'd argue matters most: `validate_settings` runs when the application is *built*. An app built in development whose settings are later changed — a reload, a long-lived process, a test — would otherwise keep serving a route that mints admin sessions. "Unreachable" is a property of today's wiring; this isn't. Disabled, it answers **404, not 403**: a deployment without this feature should not advertise that the feature exists. ## Reusing the real session path As the issue asks, and it is the point. Same `SessionRepository.create`, same `create_session_cookie`, same signer — so revocation (#61), the sliding idle window and the absolute ceiling all apply, and #12's journey will exercise what a reviewer actually holds rather than a parallel mechanism that drifts from it. Asserted directly: the cookie decodes to a real `user_session` row, logout ends it, a viewer session is refused a reviewer route the same way, and what it writes is attributed to it. Two details worth naming: - **The account is namespaced** as `dev-login:<email>` in `provider_sub`. That column is the join key to the identity provider; a bypass account sharing the namespace could collide with a real one, or be mistaken for one later by someone auditing who had access. - **Re-signing-in switches the role** on the same account rather than creating a second user, so a test can assert "the same person cannot do this" — which is most of what a role matrix is for. Every use logs at warning level with the identity and role. ## Frontend The sign-in page grows a loudly labelled panel when the backend reports the bypass is on. **A failed probe is treated as off** — defaulting the other way would put an authentication-bypass form on the one page an unauthenticated stranger can always reach. The status endpoint discloses exactly one boolean, and that is asserted so it does not grow into a description of how the deployment is configured. ## A real defect fell out Writing the "it logs its use" assertion found that **`alembic/env.py` was switching off the whole application's logging.** It called `fileConfig` with the generated template's default `disable_existing_loggers=True`, which sets `.disabled` on every logger that already exists — permanently, with no error and nothing to notice. The symptom was a test that passed alone and failed in a full run: alone, the route's module hadn't been imported when the migration ran, so there was nothing to disable. It reaches production through `app/cli/`, whose tools migrate and then work in the same process, and it would have quietly undermined #89. `test_harness_isolation.py` now asserts both the flag and that a record actually reaches a handler afterwards. ## Done when - [x] A developer can sign in locally with no OAuth credentials configured - [x] The bypass is off by default and refuses to start under production - [x] Sessions issued by the bypass are indistinguishable downstream from OAuth sessions - [x] Role can be chosen so reviewer and admin paths are both testable - [x] The README no longer advises editing the database by hand **1020 passed, 8 skipped** backend; **58** frontend; ruff and `tsc` clean. Note on naming: the issue says `CIRCA_ENV`, but the existing setting is `CIRCA_ENVIRONMENT` and that is what both this and #14 use.
Sign in to join this conversation.
No description provided.