Commit Graph
9 Commits
Author SHA1 Message Date
Evan JarrettandClaude Fable 5.1 dcee8f6a62 deps: upgrade every module; gate loopback OAuth tests on testmode
go get -u across the root, scanner, and deploy modules, then tidy. The
credential helpers pin atcr.io v0.1.4 for standalone go install and are
left alone (go work sync tried to strip that pin; reverted). Direct
upgrades in the root: indigo 20260901 to 20260903, aws-sdk-go-v2 core
1.45.1 to 1.47.0 with config, credentials, and s3 alongside, x/crypto
0.55 to 0.57, x/net, x/sync, x/sys, x/image, klauspost/compress 1.20,
go-containerregistry 0.22.1, goldmark 1.8.6, regclient 0.11.6 (pinned
only by the integration-tagged package, so the bulk upgrade skipped it).
Scanner and deploy had no direct updates; their indirect sets moved.

The indigo delta is a hardening series: identity.DefaultDirectory and
oauth.NewClientApp now carry an SSRF-guarded transport that refuses
loopback and private ranges, did:web and well-known bodies are size
capped, all auth-server endpoints must be HTTPS URLs, and MST decoding
validates PrefixLen on untrusted nodes. Production is unaffected. The
testmode seam in pkg/atproto absorbs the rest: a probe confirmed an
untagged build now refuses 127.0.0.1 with indigo's unsafe-address error
and a tagged build dials through.

Two OAuth tests drove the real client against httptest servers on
loopback and failed untagged after the bump; three siblings in the same
fixtures passed only because the refused dial happened to satisfy a
"transient error" assertion. All five, with their fixtures and fake
stores, move under //go:build testmode in sibling files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwYzaG3Yy7uA8FbZ5qk3tQ
2026-09-11 16:41:58 -05:00
Evan JarrettandClaude Opus 5 500ee2f8d1 auth: classify service-token failures structurally, not by string
Follow-up to 37bab32. That commit stopped deleting OAuth sessions on transient
errors, which fixed spurious sign-outs but overshot on one path: a genuinely dead
session stopped being evicted at all, turning a forced re-login into a permanent
failure loop.

GetOrFetchServiceToken flattened every non-200 from getServiceAuth into
fmt.Errorf("service auth failed with status %d: %s"). IsSessionInvalidError then
had nothing structured to inspect, and its string fallback could not help: it
looks for the OAuth 2.0 code invalid_token, while atproto emits the XRPC name
InvalidToken. The difference is the underscore, not the case, so lowercasing
never bridged it. A revoked session came back 401 InvalidToken and was classified
transient, so /auth/token returned 503 forever and the user was never prompted to
re-authenticate.

The non-200 branch now wraps an *atclient.APIError carrying the status and the
parsed atproto error name, which is what the existing structured checks in
IsSessionInvalidError already know how to read. Transient shapes stay transient:
atprotoErrorName returns "" for a non-JSON body, so 500s with HTML, 502s, and
429s do not evict.

ExpiredToken is deliberately not treated as a dead session. It means "refresh
me", and deleting on it would sign the user out of every UI session over an
ordinary access-token expiry a refresh would have fixed. isAuthError omits it for
the same reason; the two classifiers have to agree about the same condition.

The comment on the string fallback claimed it was a looser spelling of the
structured check. It is not — it handles a different error family. indigo's
RefreshTokens returns OAuth token-endpoint failures as a bare fmt.Errorf carrying
the auth server's snake_case code verbatim ("token refresh failed (HTTP 400):
invalid_grant"), never a typed error, so a string match is the only thing that
can classify a refresh failure, which is the invalid_grant replay case 37bab32
exists to detect. Both comments now say which family they cover.

Two hardening items on the same theme:

use_dpop_nonce no longer counts as an auth error in the appview's isOAuthError.
It is a routine handshake step indigo retries with the server-supplied nonce, and
treating it as fatal signed users out over ordinary nonce rotation. It can still
escape when a server sends that error with no DPoP-Nonce header, leaving indigo
nothing to retry with; a stuck session there is preferable to signing everyone
out in the common case, and the comment says so rather than claiming it cannot
happen.

Detached session deletes are bounded by SessionDeleteTimeout. They run on
context.WithoutCancel so a canceled request cannot leave the cleanup half-done,
which also stripped the only deadline they had — a wedged database write blocked
the goroutine with no way to shed it. Matches the bound already on the detached
persist callback. The unparseable-token-endpoint warning is now deduped per
endpoint rather than once per process, since that path fails open by returning
the client unwrapped, silently reinstating the refresh burn.

The refreshDetachTimeout comment now notes the cap is per-POST: the DPoP-nonce
retry means one refresh can issue two, holding the per-DID lock for up to twice
the stated value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:19:59 -05:00
Evan JarrettandClaude Fable 5 37bab324d7 fix OAuth refresh-token burn on client cancellation causing sign-outs
When a Docker client canceled a slow /auth/token request mid-refresh, the
token-refresh POST was aborted client-side but completed on the PDS, which
rotated the refresh token. The rotated token was never received or persisted,
so the next refresh replayed the consumed token, got invalid_grant, and the
session (OAuth + UI) was deleted, signing the user out everywhere.

- Detach refresh POSTs from the inbound request context via a per-session
  RoundTripper (WithoutCancel + 30s cap); once a refresh starts it completes
- Persist session updates (rotated tokens, DPoP nonces) on a detached context
- Gate session deletion on IsSessionInvalidError: cancellation, timeouts, and
  transport errors no longer delete sessions; genuine invalid_grant still does
- Add phase timing to /auth/token and per-DID lock wait warnings to attribute
  the ~14s pre-refresh stalls that push requests past Docker's deadline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 13:38:45 -05:00
Evan Jarrett efef46b15a various linting fixes 2026-01-04 22:02:01 -06:00
Evan Jarrett 8956568ed2 remove unused filestore. replace it with memstore for tests 2025-12-29 16:51:08 -06:00
Evan Jarrett 15d3684cf6 try and fix bad oauth cache 2025-11-08 20:47:57 -06:00
Evan Jarrett 6793ba6a50 use confidential oauth in production 2025-10-29 12:06:47 -05:00
Evan Jarrett b0799cd94d unit tests 2025-10-28 17:40:11 -05:00
Evan Jarrett 08fb8abb41 invalidate sessions when scopes change 2025-10-21 09:54:23 -05:00