Skip to content

v1.5.0 - OAuth client identity: probed CIMD with DCR fallback, and explicit modes

Choose a tag to compare

@pacnpal pacnpal released this 17 Aug 14:42
· 20 commits to main since this release
f48bff1

OAuth client identity: probed CIMD with DCR fallback, and explicit modes

Signing in to a remote OAuth server offered the instance's CIMD URL-based client id
whenever the base was https — but an https base doesn't prove the client-metadata
document is publicly fetchable. An instance behind an auth-gating proxy (Cloudflare
Access, an oauth2-proxy, HTTP basic auth) serves the document fine to the operator's
signed-in browser while answering the provider's unauthenticated server-side fetch
with a 401 — and the failure only surfaced after the browser had been sent away, as
the provider's opaque "Client metadata is temporarily unavailable" page. 1.5.0 makes
the sign-in verify reachability first and fall back to Dynamic Client Registration
when the document is gated, and adds an explicit client-identity choice — instance-wide
and per-server — for deployments where the probe can't know better.


CIMD self-probe with automatic DCR fallback (#119)

Before offering the URL-based client id, begin_authorization now fetches its own
/api/oauth/client-metadata.json exactly the way the authorization server would:
unauthenticated, no cookies, no redirects.

  • Only a definitive bad answer withholds the offer — a stable gate shape (401/403,
    a login redirect, 404) or a 200 whose body isn't this instance's document. The flow
    then falls back to DCR silently, so a gated instance signs in with no infra
    change
    ; a warning log names the likely gate and the remedy.
  • Inconclusive evidence keeps the offer. A connection failure proves nothing —
    plenty of deployments can't hairpin their own public hostname from inside the
    container while the provider reaches it fine — and neither does a transient status
    (408/429/5xx). A deadline that expires before response headers classifies as a
    connection failure; one that expires after them means the document can't be
    delivered in a provider-compatible budget, which withholds.
  • Bounded hard. The probe is wall-clock-capped (asyncio.wait_for, 8s) and reads
    raw, identity-encoded bytes capped at 64 KiB — no decompression ever runs, so a
    stalling or bomb-shaped response can't hold /oauth/authorize open or balloon
    memory.
  • Cached. Conclusive verdicts are kept per URL for 5 minutes — every server on an
    instance shares one metadata URL, so consecutive sign-ins don't re-pay the
    round-trip. Inconclusive answers are never cached.

Explicit client-identity modes, instance-wide and per-server (#119)

The probe errs on the side of offering, and some deployments know better in either
direction — so the choice can now be pinned.

  • Settings → Upstream OAuth client identity (upstream_oauth_client_mode):
    auto (the probed default), CIMD (always offer the URL client id, probe-free —
    for documents that are public even though the container can't confirm it), or DCR
    (never offer it).
  • Per server: a new oauth_client_mode column (inherit | auto | cimd |
    dcr), edited via a Client identity select in the server form's client-credentials
    section. inherit (the default) follows the Settings choice; a static client id
    bypasses all of it.
  • A pinned mode actually takes effect. Forced cimd ignores a stored DCR
    registration (the SDK only consults the URL client id when no client is seeded);
    forced dcr never offers the URL, seeded or not.
  • Like idle_timeout_s, the column is a sign-in concern only: outside config_hash
    (changing it never bounces a bridge) and outside the OAuth signature (never wipes
    working tokens).

Fixes and hardening

  • A persisted CIMD identity is no longer reused as a registered client. A prior
    CIMD sign-in stores client info whose client_id is the metadata URL; seeding it
    bypassed the SDK's CIMD/DCR decision and would have resent a gated client id on
    every re-authentication. It's excluded from reuse — there's no registration quota to
    protect, the SDK recreates it locally for free — and stale stored identities
    self-heal on the next sign-in.
  • Second-click superseding now covers the whole begin. The flow registers in
    _PENDING before its first await, so an Authenticate click (or a delete/config-edit)
    landing while a flow is parked in the probe cancels it; a superseded flow refuses to
    start driving (409) instead of racing the winner's token promotion.

Upgrade notes

  • No migration and no new environment variables. oauth_client_mode arrives via
    the existing forward-only ADD COLUMN path; pre-column rows read as inherit, and
    nothing enters config_hash, so no bridge restarts on upgrade.
  • Gated instances start working, everything else is unchanged. Under the default
    auto, an instance whose metadata document is publicly fetchable keeps using CIMD
    exactly as before; one behind an access gate now completes sign-in via DCR instead
    of failing at the provider. To use CIMD behind a gate, exempt
    /api/oauth/client-metadata.json from it (the document is public by design and
    carries no secrets) or pin CIMD explicitly.

Full Changelog: v1.4.1...v1.5.0

License

MIT © pacnpal