Skip to content

Graceful re-authentication: in-place recovery + reauth= policy #156

Description

  • Goal: let a caller recover from a dead session (the 8h wall, or a revoked / idle-expired
    refresh token) without reconnecting, and optionally let the driver drive that recovery
    automatically — separating re-auth policy (the user's) from mechanism (the driver's). Purely
    reactive and daemonless, so it ships right after the PoC.
  • Scope (in):
    • Mechanismholder.reauthenticate(timeout) (surfaced as Connection.reauthenticate()):
      synchronous, routed through the same single-flight gate as refresh, it re-runs login()
      (transient callback server + browser + code exchange) and swaps in a fresh TokenSet, resetting
      the 8h clock. Process-wide, so one re-auth re-arms every Connection and N concurrent callers at
      the wall collapse to one browser bounce.
    • Policy knob — a connect()-time reauth= selector over the ReauthenticationRequired
      exception (a typed OperationalError subclass, already raised by child 3):
      • "raise" (default) — auth_flow raises; the app catches it (anywhere on the DBAPI surface)
        and either calls connection.reauthenticate() then retries, or reconnects. The driver never
        pops a browser unbidden — the settled "don't ambush a live query" rule.
      • reauth=<callback> — the user's registered "begin browser re-auth" intent: on the wall, the
        first thread through the gate invokes on_reauth_required(ctx), which calls
        ctx.reauthenticate() to consent + drive (blocking), or returns / raises to decline. ctx
        carries the reason (idle / absolute / revoked) + org so the callback can pre-notify a human,
        log, or marshal to a UI thread — but not the authorize_url / code plumbing, which stays
        the driver's.
  • Out of scope: proactive re-auth ahead of the wall (child 8 — needs the daemon); the daemon
    itself (child 7). Even here re-auth still raises on timeout / cancel / failure — the callback
    only demotes the exception from default outcome to last resort.
  • Depends on: children CCloudOAuth provider: login + on-request refresh + two auth views #153, Process-wide holder: one OAuth identity per process #154, connect() wiring for interactive OAuth #155 (the login() mechanism + single-flight gate, the holder, and the
    connect() surface the knob rides on).
  • Prior art: mcp-confluentOAuthHolder.ensureLoggedIn() already re-logs in on demand once
    refreshTokenExpired() / hasNonTransientError(), which is exactly our reactive path; we add the
    explicit reauthenticate() entry point and the reauth= policy over it. ide-sidecar — signals
    re-auth-needed (connection → NONE / FAILED) and leaves the client to restart sign-in: the
    "raise and let the app decide" default, without an in-place recovery call.
  • Sized right: one holder method reusing login(), the reauth= validation, and a typed
    exception; no daemon, and no new async surface — the browser round-trip is already behind
    wait_for_code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions