Skip to content

OIDC sign-in for the web dashboard: give the web surface a per-user identity instead of one shared token #2550

Description

@erikdarlingdata

The web dashboard authenticates with one shared bearer token and has no notion of who is looking at it. That is fine for a single operator on a laptop and increasingly wrong for the way it is actually being used — a network-exposed dashboard several people open, with write paths behind it.

What exists today

DarlingWebHostService.DecideWebAuth is a pure function over three facts, and the matrix is pinned in DarlingWebAuthTests:

  • Network mode — a caller presents ?token= (constant-time compare), which is exchanged for an HMAC-signed HttpOnly SameSite=Strict session cookie and 302-redirected to strip the token from the URL. Out-of-CIDR is 403 even with a valid cookie and token. No credential gets a minimal inline login form.
  • Loopback still requires a cookie or token (DECISION NEEDED: web dashboard's tokenless-loopback allow now covers write endpoints #1649) — it is not a free pass. A loopback-only dashboard registers no auth middleware at all and is tokenless.
  • Always on: a Host-header allowlist (anti-DNS-rebind) and SameSite=Strict.

That is a well-built gate, and none of it is what this issue proposes to remove. The problem is what the credential is.

Three concrete consequences

1. There is no identity, and the code says so. DarlingWebEndpoints.cs:634:

The updated_by stamp for a web edit. The web surface has no per-user identity, so a constant is honest — it marks the row as web-authored.

Every custom view anyone creates or edits is stamped web. The provenance column distinguishes web from MCP, which is real information, but it cannot answer "who changed this dashboard" because nothing in the request carries that.

2. The shared token gates writes, not just reads. Any authenticated seat can create, update and delete custom views, and the alert-tuning surface changes configuration the service delivers on. The comment at DarlingWebEndpoints.cs:156 is explicit that editing is deliberately available to any authenticated seat, on the reasoning that a network caller has already passed the token→cookie + CIDR gate. That reasoning is sound given one operator. With one shared secret and several people, it means no way to grant read-only access to anyone.

3. The token is, by design, readable by anyone who can log on to the host. From DarlingPrintEndpointTokenTests, in the repo's own words: it is a DPAPI LocalMachine-scope blob with an entropy constant compiled into an open-source binary, and darling.json deliberately grants INTERACTIVE read — so "an ordinary logged-on NON-administrator can already read and decrypt the blob in four lines of PowerShell." That is an accepted and correctly-documented property of the endpoint tokens. It is a much less comfortable property for the only credential standing between a LAN client and the dashboard's write paths.

And revocation is all-or-nothing: taking access away from one person means rotating the token and re-onboarding everyone, which is exactly the failure #2479 added --print-web-token to stop people hitting by accident.

What OIDC buys

  • Identityupdated_by becomes a real subject instead of the constant web, with no schema change (the column already exists and already carries a provenance string).
  • Per-user revocation — deprovision in the IdP, done. No shared-secret rotation, no re-onboarding.
  • Read-only seats — map IdP groups to a role, and gate the custom-view CRUD and alert-tuning writes on it. This is the first time the product could offer "let the team look, let two people change things."
  • MFA and conditional access for free, from whatever the org already runs.

The prerequisite that probably splits this in two

The web listener is plain HTTP. grep -c UseHttps across Darling/ returns 0; DarlingWebHostService calls options.Listen(...) with no TLS configuration at all.

That matters because most IdPs refuse a non-HTTPS redirect_uri — with the near-universal exception of http://localhost, which is exactly the loopback case and therefore the only place OIDC would work as things stand. Network mode, which is the mode that actually needs OIDC, is the mode that cannot have it yet.

So this likely wants to be two issues: an HTTPS listener (cert source, config shape, what happens on renewal, whether it is opt-in), and then OIDC on top. Worth deciding before any of it is designed, because if TLS is not on the table then this issue is loopback-only and largely pointless.

Open questions, none of which should be answered by me alone

  • Does the shared token stay as a fallback for headless/scripted callers and the loopback case, with OIDC as an opt-in for network mode? Leaning yes — dropping it would break anyone scripting against the web endpoints, and the tokenless-loopback path is a deliberate ergonomic choice. But two credential paths into one surface is exactly where auth bugs live, so the matrix in DarlingWebAuthTests would need to grow to cover it rather than being bolted around.
  • Which flow — authorization code + PKCE is the obvious answer for a browser app.
  • Where does client config livedarling.json's web.network block alongside the token, or the store's config_service so it is editable without touching the box?
  • Does the CIDR gate stay in front of OIDC? I would keep it. Defence in depth, and it is the thing that currently makes an out-of-CIDR request 403 even holding a valid credential.
  • MCP is unaffected — it has its own separate network block and token, and this issue does not propose changing it.

Related: #2479 (endpoint token reprint, the same shared-secret ergonomics), #1649 (loopback still requires a credential), #1562 (the current browser auth model).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions