Skip to content

Report first-run 'no accounts yet' from the login endpoint instead of a polled endpoint #361

Closed

Description

@mforce

Follow-up to #283 and PR #359. Replaces the mechanism #359 shipped; the user-visible outcome is unchanged.

What #359 does today

GET /api/v1/auth/provisioning — anonymous, returns {provisioned: bool} — polled by the SPA on every login-page mount, so the login screen can say that no account exists yet.

Proposal

Delete that endpoint. Instead, when a login attempt fails and the account count is zero, have the login response carry a distinct error code (e.g. Auth.NoAccountsProvisioned), and let the SPA map that code to the existing copy in the error area it already renders.

Why it's better

The single biggest finding on #359 — raised independently by codex (P1) and a review agent — was that an anonymous, unauthenticated endpoint reaches the database on every request throughout the pre-provisioning window, with no global limiter behind it. #359 mitigated that with a dedicated auth-provisioning rate-limit policy. This removes the surface instead of rationing it.

Login is already rate-limited (10 per 15 min per IP, #143) and already performs a PBKDF2 hash, so one cheap EXISTS on the failure path is negligible next to what that request already costs.

It also narrows disclosure. The polled endpoint answers anyone who asks, without them attempting a sign-in. The proposed version only speaks to someone who actually tried, and only when the count is zero — a state in which no credential exists to protect, and which reveals nothing about any individual account. Once one account exists, behaviour is byte-identical to today's non-enumerating 401.

Deletes

  • GET /api/v1/auth/provisioning and ProvisioningStatusResponse
  • the auth-provisioning rate-limit policy (RateLimitingOptions.ProvisioningPolicyName, its FixedWindow, its registration and validation)
  • getProvisioningStatus in web/src/api/client.ts and the login-page mount effect
  • tests/Cluckwork.Api.IntegrationTests/ProvisioningStatusTests.cs

Keeps

  • FirstRunStatusService and FirstRunProvisioningLatch — same query, called from the login path instead of an endpoint. The latch still matters: it keeps the check off the failure path once an Owner exists.
  • The notice copy (auth:noAdminYet / auth:noAdminYetHint), now rendered from the login failure rather than on mount. It stays free of any command or deployment detail (see feat(auth): tell the operator when an instance has no admin yet #359).
  • README, GLOSSARY and Help page wording — all already phrased in terms of what the screen tells you, not the mechanism.

Tradeoff, accepted

The operator no longer learns before attempting. They land on an ordinary form, try to sign in, fail, and are then told. One extra step, and arguably the right audience: only someone actually trying to sign in needs to know.

Tests worth having

  • zero accounts + failed login → the distinct code; one account + failed login → the ordinary non-enumerating 401, byte-identical (both sides of the boundary — a one-sided test would pass against a build that always emits the code)
  • a successful login never emits it
  • the check does not run once the latch is set
  • the SPA renders the notice copy for that code and the ordinary invalid-credentials copy otherwise

Activity

  1. added this to the Phase 1.5 — Hardening milestone on Aug 2, 2026
  2. mforce commented on Aug 2, 2026

    @mforce
    OwnerAuthor

    Closing — filed in error. This is not separate work: it's the same #283 follow-up as PR #359, just a better mechanism for it. Tracking stays on #283, and the implementation moves to a replacement PR; #359 will be closed as superseded rather than merged.

  3. mforce commented on Aug 2, 2026

    @mforce
    OwnerAuthor

    Reopening — this is the tracking issue for #363, which implements it. Disregard my earlier close comment; the work is real and is in review now, it just belongs to one issue rather than a second one alongside #283.

  4. reopened this on Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions