v1.5.0 - OAuth client identity: probed CIMD with DCR fallback, and explicit modes
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/authorizeopen 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), orDCR
(never offer it). - Per server: a new
oauth_client_modecolumn (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
cimdignores a stored DCR
registration (the SDK only consults the URL client id when no client is seeded);
forceddcrnever offers the URL, seeded or not. - Like
idle_timeout_s, the column is a sign-in concern only: outsideconfig_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 whoseclient_idis 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
_PENDINGbefore 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_modearrives via
the existing forward-onlyADD COLUMNpath; pre-column rows read asinherit, and
nothing entersconfig_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.jsonfrom it (the document is public by design and
carries no secrets) or pinCIMDexplicitly.
Full Changelog: v1.4.1...v1.5.0
License
MIT © pacnpal