Skip to content

Introduce OAuth authentication for Logfire#6

Open
jirikuncar wants to merge 2 commits into
mainfrom
jiri/oauth-support
Open

Introduce OAuth authentication for Logfire#6
jirikuncar wants to merge 2 commits into
mainfrom
jiri/oauth-support

Conversation

@jirikuncar
Copy link
Copy Markdown

@jirikuncar jirikuncar commented May 14, 2026

Summary

  • Add OAuth Device Authorization Grant (RFC 8628) login flow as an alternative to LOGFIRE_TOKEN, so users can authenticate the plugin against Logfire without manually managing a long-lived write token.
  • New scripts/auth.py CLI (login/logout/status/refresh) wired up via /logfire-session-capture:login, /logfire-session-capture:logout, /logfire-session-capture:status, /logfire-session-capture:refresh slash commands.
  • New scripts/oauth_token.py shared module: persists a single token bundle at ~/.logfire/claude-code-logfire-plugin.json (recording its own base_url), auto-refreshes when within 60s of expiry, and serialises refresh-token rotation via an os.mkdir lock so concurrent sessions don't race.
  • scripts/log-event.py now resolves auth in this order: LOGFIRE_TOKEN env var (highest precedence, backwards-compatible) → stored OAuth bundle → skip OTLP export silently.

Test plan

  • python3 scripts/auth.py login completes the device-code flow and writes ~/.logfire/claude-code-logfire-plugin.json
  • python3 scripts/auth.py status reports the token expiry / scope
  • python3 scripts/auth.py refresh rotates the access token
  • python3 scripts/auth.py logout removes the bundle
  • With only an OAuth bundle (no LOGFIRE_TOKEN), running a full SessionStart → Stop → SessionEnd hook lifecycle exports spans to Logfire
  • With LOGFIRE_TOKEN set, the env var still wins over the stored bundle
  • With no auth source available, hooks run without errors and JSONL fallback still works

@jirikuncar jirikuncar requested a review from petyosi May 14, 2026 16:45
@jirikuncar jirikuncar self-assigned this May 14, 2026
@jirikuncar jirikuncar added the enhancement New feature or request label May 14, 2026
Copy link
Copy Markdown
Member

@petyosi petyosi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SessionEnd hot-path regression risk

The recent commit 0f1b117 ("Close the session trace before /exit kills the SessionEnd hook") was specifically designed so the eager send_otlp for the root span (scripts/log-event.py:1167) runs before any locks or slow work — Claude Code aggressively kills hook subprocesses when /exit shuts the parent down, so the finalized root span has to ship first.

This PR moves OAuth resolution into main() (scripts/log-event.py:1269-1283), which runs before the dispatch into handle_session_end. When the access token is within 60s of expiry, that adds a synchronous refresh to the hot path:

  • lock acquisition: up to 5s (oauth_token.py:81, LOCK_MAX_WAIT_SECONDS)
  • discover_metadata GET: 10s timeout (oauth_token.py:209)
  • _http_post_form refresh POST: 30s timeout (oauth_token.py:218)

Worst case ~45s before the eager root-span send even starts, against a 30s SessionEnd budget. In practice the refresh window is narrow (60s before expiry) so most sessions won't hit it — but when they do, /exit will kill the subprocess mid-refresh and the trace silently won't close, which is exactly the failure mode 0f1b117 was meant to prevent.

A few possible mitigations, any one would help:

  1. Plumb a short timeout through get_access_token_refresh_http_post_form / discover_metadata, and pass ~3s from the hook hot path (the CLI subcommands can keep the current 30s).
  2. Cache token_endpoint in the stored bundle on login, so refresh skips the discovery roundtrip entirely.
  3. Skip lazy refresh in SessionEnd specifically: if the access token has any validity left, ship the trace first and let the next session's SessionStart do the refresh.

(3) is the smallest change and arguably the cleanest — SessionEnd is a one-shot finalization and doesn't benefit from a fresh token vs. a 30s-from-expiry one.

The eager root-span send added in 0f1b117 had to ship before any blocking
work, but the OAuth resolution path in main() could synchronously refresh
a near-expiry token (lock + discovery + token POST, up to ~45s) before
the eager send ever ran — defeating the guard the SessionEnd hook needs
against /exit killing the subprocess mid-finalisation.

Two fixes:

- Skip the lazy refresh on SessionEnd if the access token still has any
  validity left (`skip_refresh_if_valid` on `get_access_token`). The
  trace ships first; the next session's SessionStart handles the refresh.
- Cache `token_endpoint` in the stored bundle at login (and on the first
  refresh of a legacy bundle), so the refresh path skips the RFC 8414
  discovery GET. Login already has the metadata in hand.

Also fix a stale docstring example: the RFC 9728 `resource` value is
`https://logfire-us.pydantic.dev/v1`, not the `/v1/traces` endpoint URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants