Skip to content

feat(auth): Sign in with AT Protocol (Bluesky)#509

Draft
hmans wants to merge 15 commits into
mainfrom
hmans/atproto-identity-research
Draft

feat(auth): Sign in with AT Protocol (Bluesky)#509
hmans wants to merge 15 commits into
mainfrom
hmans/atproto-identity-research

Conversation

@hmans

@hmans hmans commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds "Sign in with AT Protocol" as a third login path (alongside password and OIDC), using 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 — no account:email scope yet, no account linking, OAuth credentials revoked immediately after identification.
  • Documents the rollout: ADR-032 (External Identity Integration Boundaries — authn-only providers, prefer protocol-level data sources over vendor CDNs) and FDR-026 (Sign in with AT Protocol), plus minor cross-linking updates to FDR-018/022/023.
  • Three small .claude/skills/ improvements that fell out of the audit: ADR/FDR triage heuristics so principles don't leak into feature docs, and chatto-finalize-pr now runs /adr before /fdr instead of in parallel.

Test plan

  • mise test-cli green (new core/atproto_test.go + http_server/atproto_test.go)
  • svelte-check clean
  • Manual sign-in verified end-to-end against bsky.social via the loopback dev URL
  • Reviewers: try a handle on a non-Bluesky PDS if you have one (exercises the "no app.bsky.actor.profile record" degradation path)

hmans added 9 commits May 19, 2026 16:44
- 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
@hmans hmans marked this pull request as draft June 17, 2026 18:18
hmans added 6 commits July 6, 2026 18:28
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant