feat(auth): Sign in with AT Protocol (Bluesky)#509
Draft
hmans wants to merge 15 commits into
Draft
Conversation
- Adds an OAuth-based "Sign in with AT Protocol" path alongside password
and OIDC, using github.com/bluesky-social/indigo/atproto/auth/oauth.
- Stores DID ↔ user link in INSTANCE KV under `user_by_atproto.{sha256(did)}`,
mirroring the OIDC subject index. Login is derived from the handle, with
numeric collision suffixing.
- On first sign-in, best-effort fetches `app.bsky.actor.profile` from the
user's own PDS (no Bluesky-Inc dependency) and mirrors display name +
avatar; missing profiles degrade silently to handle-only accounts.
- New `[auth.atproto] enabled` config + GraphQL `enabledAuthProviders`
surfaces `"atproto"`; frontend login page gains a handle input with a
loading state and submits to `/auth/atproto`.
- Dev orb.local stacks can't satisfy ATProto's "public URL or loopback"
spec, so the bootstrap-tag build pins the OAuth URL to
http://127.0.0.1:5173; release builds always use webserver.url.
…y-research * origin/main: chore(dev): replace docker compose with mise dev for Conductor (#505) fix(rbac): rename PermissionLevel to SERVER/GROUP/ROOM, fix group mislabel (#503) fix(frontend): keep WS retry loop alive so tab-resume reconnects work (#502) chore: skill cleanup, /chatto-checkup, narrow ARCHITECTURE.md (#500) fix(frontend): align avatars, icons, and text across rows (#501) refactor(core): split rooms.go into focused per-concern files (#499) docs(fdr): introduce Feature Decision Records and migrate feature docs (#498) 0.0.177 # Conflicts: # compose.yml
The compose.yml-based dev stack (replaced by mise dev in #505) was the only reason ATProto needed a build-tag-gated URL override: orb.local hostnames couldn't satisfy the OAuth spec's "public URL or loopback" requirement. The new mise dev setup uses http://localhost:$CONDUCTOR_PORT out of the box, which the existing isLocalhostURL detection picks up natively — so the override is now dead weight (and would have been wrong, since it hardcoded port 5173 instead of $CONDUCTOR_PORT). go.mod change: indigo moved indirect → direct after mise test-cli ran go get ./...; we import the package directly, so this is correct.
- ADR-032 records the two architectural principles that ATProto sign-in surfaced: external identity providers are authn-only (tokens are dropped after identification), and we prefer protocol-level data sources (user PDS) over vendor-hosted convenience APIs (Bluesky CDN). Both already implicit in OIDC; codifying them keeps future provider integrations on the same footing. - FDR-026 documents the ATProto sign-in feature itself: behavior, DID-as-foreign-key, handle-derived login with collision suffixing, first-sign-in profile mirroring, the deliberate Phase-1 omission of the account:email scope. - FDR-023 updated to mention ATProto as a third sign-in path alongside password + OIDC, cross-linking FDR-026 and ADR-032. - FDR-022 updated to note SSO-driven profile seeding on first sign-in (covers both OIDC's picture claim and ATProto's bsky profile). - FDR-018 updated to enumerate all three signup paths (password, OIDC, ATProto) and call out that ATProto signups currently land without a verified email.
Three small fixes surfaced by the ATProto PR audit:
- FDR skill: new Triage section with heuristics for deciding whether a
Design Decision belongs in an FDR or an ADR. The smell test ("we
never…", "we always…", "any future…") catches architectural principles
that drift into feature docs.
- ADR skill: parallel Triage section, plus the previously-missing
"What an ADR Is / Is NOT" sections (the FDR skill had these; the ADR
skill was a pure how-to-store guide with no scoping guidance).
- chatto-finalize-pr: run /adr before /fdr instead of in parallel.
Architectural decisions inform feature docs; running ADR triage first
lets new ADRs get extracted before FDR drafts start citing them.
Parallelization was producing FDR drafts containing principles that
should have been ADRs.
- Adds 'account:email' to the requested scope set. On first sign-in, if the user grants the scope on the PDS consent screen and their PDS-side email is confirmed, the email is added to the new Chatto account as verified — which also triggers owners.emails owner auto-promotion via the shared addVerifiedEmail hook. - The scope is independently grantable: a user can deny just account:email and still complete sign-in. The seeding step becomes a no-op in that case. Same shape if the getSession fetch fails or the email is unconfirmed. - maybeSeedEmail runs only on first sign-in (inside the isNewUser branch), matching the rest of the SSO-seed-once-then-locally-owned pattern. Existing ATProto users who grant the scope retroactively won't have their email overwritten. - Updates FDR-026 to reflect the new scope ask + the decline-gracefully behavior, and removes the "no email" Phase 1 caveat from FDR-018's AT Protocol signup bullet.
- LinkATProtoDID only treats jetstream.ErrKeyExists as a potential idempotent re-link; other Create errors (transient NATS failures, etc.) are now wrapped and returned instead of being misreported as ErrATProtoDIDAlreadyClaimed. - Remove unused sync.Mutex field from atprotoHandler.
Replaces indigo's MemStore with a NATS-KV-backed implementation of
oauth.ClientAuthStore. ATProto sign-in now survives server restart
mid-flow and works correctly in multi-replica deployments — the
callback can land on a different replica than the one that started
the flow and still find the auth-request state.
Storage shape:
- AUTH_TOKENS bucket, under prefixes atproto.auth_request.{state}
(10-minute per-key TTL via jetstream.KeyTTL — the bucket already
has LimitMarkerTTL set for the existing 5-min authorization-code
TTL) and atproto.session.{sha256(did + ":" + session_id)} (bucket
default TTL).
- Already in the backup skip-list as 'security (prevents token
leakage)' — ATProto state inherits that.
- Stored values are protobuf, defined in
proto/chatto/atproto/v1/store.proto.
Layering:
- Core owns the KV ops (atproto_oauth_store.go) and deals only in
protobuf types — no indigo import.
- HTTP handler has a small adapter (atproto_store.go) implementing
oauth.ClientAuthStore by converting between indigo's structs and
our protobuf shapes.
The session entries contain plaintext access tokens, refresh tokens,
and DPoP private keys — same exposure as the previous MemStore for
the same short window, since we still revoke immediately after the
callback. A note in the proto and at the top of the core file calls
this out so any future feature that retains sessions wraps the
secret fields via the KMS service (ADR-007 territory).
…y-research * origin/main: 0.0.178 fix(frontend): restore per-server last-room memory on server re-entry (#511) style(admin): give panels a tinted header band, ridge, and lift (#510) fix(frontend): use base font size for username change notice (#508) feat(frontend): add server overview entries to Cmd-K quick switcher (#506) feat(frontend): move current user info into secondary sidebar (#507) refactor(core): store verified emails as per-email proto entries (#504) # Conflicts: # docs/fdr/INDEX.md
…y-research * origin/main: (663 commits) docs: update project status guidance (#1321) fix(frontend): hydrate room lifecycle event actors (#1319) docs: finalize 0.4.0 release notes (#1317) chore(main): release 0.4.0 (#1309) chore(release): always update release pull requests (#1316) chore(license): make root license scanner-friendly (#1315) chore(license): configure mixed repository licensing (#1304) refactor(dev): simplify mise dev task (#1314) docs(release): update 0.4.0 release notes (#1313) fix(frontend): use direct ticketed asset URLs (#1312) chore(ci): restore stable release-please workflow (#1311) fix(frontend): share unread marker lifecycle with threads (#1310) fix(frontend): stabilize new messages separator (#1308) feat(frontend): add Trusted Types markdown policy (#1307) chore(main): release 0.4.0-beta.14 (#1300) refactor(api)!: consolidate ConnectRPC surface (#1306) feat(api)!: clean up server assets calls and includes (#1303) feat(api)!: consolidate shared api shapes (#1302) fix(frontend): sync presence badge across tabs (#1301) feat(api)!: consolidate shared public API types (#1299) ... # Conflicts: # .claude/skills/chatto-finalize-pr/SKILL.md # .claude/skills~origin_main # cli/go.mod # cli/go.sum # cli/internal/config/config.go # docs/adr/INDEX.md # docs/fdr/FDR-018-account-lifecycle.md # docs/fdr/FDR-022-user-profile.md # docs/fdr/FDR-023-authentication-and-sessions.md # docs/fdr/INDEX.md # frontend/src/routes/login/+page.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bluesky-social/indigo. First sign-in keys on the user's DID and best-effort mirrors display name + avatar from the user's PDS (app.bsky.actor.profile), with graceful degradation for non-Bluesky handles. Phase 1 is intentionally minimal — noaccount:emailscope yet, no account linking, OAuth credentials revoked immediately after identification..claude/skills/improvements that fell out of the audit: ADR/FDR triage heuristics so principles don't leak into feature docs, andchatto-finalize-prnow runs/adrbefore/fdrinstead of in parallel.Test plan
mise test-cligreen (newcore/atproto_test.go+http_server/atproto_test.go)svelte-checkcleanbsky.socialvia the loopback dev URLapp.bsky.actor.profilerecord" degradation path)