Skip to content

Feat/atproto oauth#418

Open
jimray wants to merge 7 commits intoemdash-cms:mainfrom
jimray:feat/atproto-oauth
Open

Feat/atproto oauth#418
jimray wants to merge 7 commits intoemdash-cms:mainfrom
jimray:feat/atproto-oauth

Conversation

@jimray
Copy link
Copy Markdown

@jimray jimray commented Apr 9, 2026

What does this PR do?

Adds AT Protocol OAuth as a login provider so authors can sign in with their Bluesky handle or any PDS-hosted identity. Uses
@atproto/oauth-client-node with PKCE (public client, minimal atproto scope — no repo access requested).

This is a POC implementation by a Bluesky employee, using the official SDK. One goal is to surface any Cloudflare Workers
incompatibilities for upstream fixes.

Related discussion: #320

How it works

Enable in config:
emdash({
atproto: true,
})

Login flow:

  1. User enters their AT Protocol handle on the login page ("Sign in via the Atmosphere")
  2. POST to /auth/atproto/login → SDK resolves handle → DID → PDS → PAR request
  3. User authorizes on their PDS
  4. PDS redirects to /auth/atproto/callback → token exchange → user provisioning
  5. If PDS returns email: provision/link user immediately (same rules as GitHub/Google — allowed_domains gating, email
    auto-linking)
  6. If PDS doesn't return email: redirect to /complete-profile → user enters email → verification email sent → click link →
    user provisioned

Key design decisions:

  • Own route tree (/auth/atproto/*) — ATProto OAuth is incompatible with the existing [provider] pattern (requires PAR,
    dynamic discovery, handle input)
  • GitHub/Google untouched — no pluggable auth refactor
  • Reuses auth_challenges table for state/session storage (no new tables)
  • oauth_accounts entry created with provider="atproto" for consistency with existing OAuth
  • atproto_did column added to users for identity attribution
  • Email verification required for the complete-profile path (prevents claiming arbitrary emails)

New routes

Route Method Purpose
/_emdash/api/auth/atproto/client-metadata.json GET OAuth client metadata (this URL = client ID)
/_emdash/api/auth/atproto/login POST Start login: { handle }{ redirectUrl }
/_emdash/api/auth/atproto/callback GET PDS redirect target, user provisioning
/_emdash/api/auth/atproto/complete-profile POST Email collection + verification email
/_emdash/api/auth/atproto/verify-email GET Email verification link handler

Type of change

  • Bug fix
  • Feature (Discussion)
  • Refactor (no behavior change)
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm --silent lint:json | jq '.diagnostics | length' returns 0
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • [ x ] New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • [ x ] This PR includes AI-generated code

jimray and others added 7 commits April 7, 2026 19:49
Adds AT Protocol OAuth support so users can sign in with a Bluesky
handle or any PDS-hosted identity. Uses @atproto/oauth-client-node
with PKCE (public client, minimal "atproto" scope).

Core changes:
- Migration 034: adds atproto_did column to users table
- Store adapters: state and session stores backed by auth_challenges
  table (no new tables needed)
- OAuth client: NodeOAuthClient with cached instances, loopback
  client ID support for localhost dev (RFC 8252)
- Routes: client-metadata.json, login (POST handle), callback
  (token exchange + user provisioning)
- Config: atproto boolean on EmDashConfig, atprotoEnabled in manifest
- Middleware: ATProto auth routes and manifest endpoint public,
  complete-profile page bypasses auth
- Route injection: ATProto routes in injectBuiltinAuthRoutes()

User provisioning follows the same pattern as GitHub/Google OAuth:
DID lookup via oauth_accounts, email auto-linking, allowed_domains
gating for new signups.

Also registers previously unregistered migration 033 in runner.ts.
- LoginPage: handle input form with "Sign in via the Atmosphere"
  divider, conditioned on manifest.atprotoEnabled
- Router: /complete-profile route (standalone, no Shell wrapper)
- AdminManifest: atprotoEnabled field
When the PDS doesn't return an email, the complete-profile page
collects one and sends a verification email before creating the
account. This prevents users from entering arbitrary emails to
gain access via allowed_domains.

- complete-profile: validates atproto pending state, sends
  verification email with token + state params
- verify-email: new GET endpoint that verifies token, looks up
  atproto pending state, provisions user with email_verified=1
- CompleteProfilePage: email form + "check your email" state
- Dev mode: full verification URL logged to server console
Covers get/set/del, conflict handling, TTL expiration, and
cross-store isolation (state store doesn't see session entries
and vice versa).
Adds ATProto login section to the Authentication guide covering
setup, email verification flow, access control, and identity storage.
Adds atproto config option to the Configuration reference.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: ae627d0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@emdash-cms/admin Minor
emdash Minor
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-ai-moderation Major
@emdash-cms/plugin-atproto Patch
@emdash-cms/plugin-audit-log Patch
@emdash-cms/plugin-color Major
@emdash-cms/plugin-embeds Major
@emdash-cms/plugin-forms Major
@emdash-cms/plugin-webhook-notifier Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Scope check

This PR changes 1,855 lines across 27 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions
Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant