Skip to content

feat: pluggable auth providers — add AT Protocol, refactor GitHub/Google#398

Open
simnaut wants to merge 4 commits intoemdash-cms:mainfrom
simnaut:claude/atproto-pds-auth-setup-LWtQo
Open

feat: pluggable auth providers — add AT Protocol, refactor GitHub/Google#398
simnaut wants to merge 4 commits intoemdash-cms:mainfrom
simnaut:claude/atproto-pds-auth-setup-LWtQo

Conversation

@simnaut
Copy link
Copy Markdown

@simnaut simnaut commented Apr 9, 2026

What does this PR do?

Introduces a pluggable auth provider system and uses it to add AT Protocol authentication as the first plugin-based provider. GitHub and Google OAuth are refactored from hardcoded buttons into the same provider interface. All auth methods — passkey, AT Protocol, GitHub, Google — are equal options on the login page and setup wizard.

Auth provider descriptor

Every provider implements AuthProviderDescriptor:

emdash({
  authProviders: [atproto(), github(), google()],
})

Each descriptor declares an adminEntry (React components for login/setup UI), optional routes (Astro route handlers), and publicRoutes (middleware bypass prefixes). Components are distributed to the admin app via a virtual:emdash/auth-providers Vite virtual module.

What changed

Scope note: The entire pluggable auth provider system is new in this PR — none of it existed on main. The pre-existing AuthDescriptor / AuthProviderModule (transparent auth for Cloudflare Access) is unchanged.

New pluggable infrastructure (core):

  • AuthProviderDescriptor / AuthRouteDescriptor types in auth/types.ts
  • authProviders config field on EmDashConfig
  • virtual:emdash/auth-providers virtual module with generateAuthProvidersModule()
  • injectAuthProviderRoutes() for generic route injection from descriptors
  • Dynamic public routes in auth middleware (merged from all providers)
  • AuthProviderContext in admin for distributing provider UI components
  • Public /_emdash/api/auth/mode endpoint returning available providers
  • emdash/api/route-utils public export for API utilities used by plugin routes
  • finalizeSetup() shared helper for setup completion (idempotent, used by all auth callbacks)

Shared OAuth user creation (@emdash-cms/auth):

  • findOrCreateOAuthUser() extracted as a shared export — handles OAuth account lookup, email auto-linking, and user creation with canSelfSignup policy
  • Used by all OAuth providers (GitHub, Google, AT Protocol) for consistent signup gating and role assignment

Login page:

  • Removed hardcoded OAUTH_PROVIDERS array
  • All providers render from virtual module via useAuthProviderList()
  • Providers with LoginButton show in a button grid; those with LoginForm expand inline on click

Setup wizard:

  • All configured auth providers appear as equal options alongside passkey
  • Clicking a provider button either navigates directly (GitHub/Google OAuth) or expands a form (AT Protocol handle input)
  • OAuth callback is setup-aware: first user becomes admin, site config is persisted, setup is marked complete
  • First-user admin creation uses a database transaction to prevent TOCTOU race conditions
  • URL error params displayed in a banner (previously errors were silently lost)

AT Protocol (new — @emdash-cms/plugin-atproto):

  • atproto() returns AuthProviderDescriptor with routes for login, callback, setup-admin, and client metadata
  • Route handlers live in the plugin package (@emdash-cms/plugin-atproto/routes/*), not in core — removing the plugin removes the routes
  • LoginButton (compact butterfly icon), LoginForm (handle input), SetupStep exported from @emdash-cms/plugin-atproto/admin
  • Uses @atcute/oauth-node-client with PKCE (public client, no key management needed)
  • First user becomes Admin during setup; subsequent users get the configured default role (Subscriber by default)
  • allowedDIDs allowlist for access control by permanent DID identifiers
  • allowedHandles allowlist with wildcard support for org-level domain gating (e.g., *.mycompany.com). Handle ownership is independently verified via DNS-over-HTTPS and HTTP well-known resolution using @atcute/identity-resolver — the PDS's handle claim is never trusted directly
  • If both are set, matching either list grants access
  • Uses shared findOrCreateOAuthUser() with canSelfSignup policy for consistent signup gating

GitHub / Google (refactored to descriptors):

  • github() and google() return AuthProviderDescriptor with adminEntry pointing to -admin.tsx components
  • LoginButton components with provider icons, reusing core's existing [provider] OAuth routes
  • No new routes needed — they reuse core's shared OAuth infrastructure

OAuth error handling:

  • OAuth initiation route checks Referer to redirect errors back to the originating page (setup wizard vs login)
  • Error messages include which env vars need to be set when a provider isn't configured

Configuration

import { atproto } from "@emdash-cms/plugin-atproto/auth";
import { github } from "emdash/auth/providers/github";
import { google } from "emdash/auth/providers/google";

emdash({
  authProviders: [atproto(), github(), google()],
})

AT Protocol options:

atproto({
  // Permanent DID-based allowlist
  allowedDIDs: ["did:plc:abc123"],
  // Domain-based allowlist (independently verified via DNS/HTTP)
  allowedHandles: ["*.mycompany.com", "alice.bsky.social"],
  // Default role for new users (default: 10 / Subscriber)
  defaultRole: 20, // Contributor
})

New routes (AT Protocol)

  • POST /_emdash/api/auth/atproto/login — initiate OAuth flow (handle → PDS authorization URL)
  • GET /_emdash/api/auth/atproto/callback — handle PDS redirect, create/find user, establish session
  • POST /_emdash/api/setup/atproto-admin — setup wizard flow
  • GET /.well-known/atproto-client-metadata.json — client metadata (PDS fetches this)

Type of change

  • Bug fix
  • Feature (requires approved 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)
  • New features link to an approved Discussion: Feat: ATProto PDS as primary auth #320

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

image image

Test results (2026-04-08)

emdash (core): 2096 tests, 113 files — all passing

 Test Files  113 passed (113)
      Tests  2096 passed (2096)

@emdash-cms/plugin-atproto: 76 tests, 7 files — all passing

 ✓ tests/plugin.test.ts (5 tests) 3ms
 ✓ tests/atproto.test.ts (3 tests) 4ms
 ✓ tests/auth.test.ts (15 tests) 9ms
 ✓ tests/standard-site.test.ts (21 tests) 5ms
 ✓ tests/bluesky.test.ts (18 tests) 15ms
 ✓ tests/resolve-handle.test.ts (3 tests) 15ms
 ✓ tests/oauth-client.test.ts (11 tests) 123ms

 Test Files  7 passed (7)
      Tests  76 passed (76)

@emdash-cms/auth: 41 tests, 2 files — all passing

 Test Files  2 passed (2)
      Tests  41 passed (41)

@emdash-cms/admin: ⚠️ Browser tests failing due to Playwright infrastructure issue (pre-existing @vitest/browser-playwright setup error, not a regression from this PR).

Copilot AI review requested due to automatic review settings April 9, 2026 02:27
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 5f8b24e

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

This PR includes changesets to release 12 packages
Name Type
emdash Major
@emdash-cms/admin Minor
@emdash-cms/auth-atproto Major
@emdash-cms/auth Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-ai-moderation Major
@emdash-cms/plugin-atproto Major
@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

All contributors have signed the CLA ✍️ ✅
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 2,616 lines across 55 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 4 different areas (area/core, area/admin, area/plugins, area/auth). Consider breaking it into smaller, focused PRs.

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new pluggable auth provider system in emdash, adds AT Protocol authentication as the first plugin-based provider, and refactors GitHub/Google OAuth into the same provider interface so all login methods are rendered uniformly in the login page and setup wizard.

Changes:

  • Add AuthProviderDescriptor + route injection + virtual:emdash/auth-providers registry generation.
  • Add @emdash-cms/plugin-atproto auth provider (routes, OAuth client, handle/DID verification, admin UI components).
  • Refactor admin login/setup UI to render configured providers dynamically and add a public /_emdash/api/auth/mode endpoint.

Reviewed changes

Copilot reviewed 52 out of 55 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds workspace links and new dependencies for the AT Protocol plugin and demo site updates.
packages/plugins/atproto/tests/resolve-handle.test.ts Adds tests for independent handle→DID verification error paths.
packages/plugins/atproto/tests/plugin.test.ts Updates plugin descriptor tests to new expected entrypoint/format/storage.
packages/plugins/atproto/tests/oauth-client.test.ts Adds tests for AT Protocol OAuth client metadata and singleton behavior.
packages/plugins/atproto/tests/auth.test.ts Adds tests for atproto() auth provider descriptor contract/config passthrough.
packages/plugins/atproto/src/routes/setup-admin.ts Implements setup-step route to initiate AT Protocol OAuth and persist setup state.
packages/plugins/atproto/src/routes/login.ts Implements login initiation route for AT Protocol OAuth.
packages/plugins/atproto/src/routes/client-metadata.ts Serves /.well-known/atproto-client-metadata.json for AT Protocol OAuth.
packages/plugins/atproto/src/routes/callback.ts Implements AT Protocol OAuth callback, user provisioning, allowlist enforcement, setup finalization, session creation.
packages/plugins/atproto/src/resolve-handle.ts Adds independent handle resolution via DNS-over-HTTPS + well-known HTTP.
packages/plugins/atproto/src/oauth-client.ts Adds OAuth client singleton + DB-backed state/session storage support for Workers.
packages/plugins/atproto/src/env.d.ts References core locals types for plugin route context typing.
packages/plugins/atproto/src/db-store.ts Adds DB-backed store implementation for atcute OAuth state/sessions.
packages/plugins/atproto/src/auth.ts Exposes atproto() returning an AuthProviderDescriptor (adminEntry, routes, public routes).
packages/plugins/atproto/src/admin.tsx Adds provider UI components (LoginButton/LoginForm/SetupStep) for admin app.
packages/plugins/atproto/package.json Exports new auth/admin/routes modules and declares deps/peers.
packages/core/tsconfig.json Includes new *-admin.tsx auth provider admin components in TS compilation.
packages/core/src/virtual-modules.d.ts Adds typing for virtual:emdash/auth-providers and config authProviders.
packages/core/src/index.ts Re-exports new auth provider types.
packages/core/src/auth/types.ts Defines AuthProviderDescriptor, AuthRouteDescriptor, and admin export contract types.
packages/core/src/auth/providers/google.ts Adds google() provider descriptor returning adminEntry for UI integration.
packages/core/src/auth/providers/google-admin.tsx Adds Google provider LoginButton component.
packages/core/src/auth/providers/github.ts Adds github() provider descriptor returning adminEntry for UI integration.
packages/core/src/auth/providers/github-admin.tsx Adds GitHub provider LoginButton component.
packages/core/src/auth/mode.ts Re-exports new auth provider types from core auth mode module.
packages/core/src/astro/routes/PluginRegistry.tsx Passes authProviders registry into the admin app.
packages/core/src/astro/routes/api/setup/status.ts Adjusts setup status commentary for external auth mode.
packages/core/src/astro/routes/api/setup/index.ts Adjusts setup state persistence comments for provider-based flows.
packages/core/src/astro/routes/api/setup/admin.ts Merges admin info into existing setup state (preserve step 1 values).
packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts Makes OAuth callback setup-aware (first user becomes admin; finalize setup).
packages/core/src/astro/routes/api/auth/oauth/[provider].ts Improves OAuth initiation error redirect target and missing-env error messaging.
packages/core/src/astro/routes/api/auth/mode.ts Adds public endpoint exposing active auth mode + provider list + signupEnabled.
packages/core/src/astro/middleware/auth.ts Merges provider-supplied public route bypass prefixes into middleware checks; whitelists /_emdash/api/auth/mode.
packages/core/src/astro/middleware.ts Adds fast-path bypass for non-/_emdash injected provider routes like /.well-known/*.
packages/core/src/astro/integration/vite-config.ts Adds virtual module IDs and generator for auth providers registry.
packages/core/src/astro/integration/virtual-modules.ts Implements generateAuthProvidersModule() to bundle provider adminEntry exports.
packages/core/src/astro/integration/runtime.ts Adds authProviders to EmDashConfig.
packages/core/src/astro/integration/routes.ts Injects core auth mode route + provider-declared routes.
packages/core/src/astro/integration/index.ts Wires authProviders through resolved config and injects provider routes.
packages/core/src/api/setup-complete.ts Introduces shared idempotent finalizeSetup() helper.
packages/core/src/api/schemas/setup.ts Adds schemas for AT Protocol login/setup request bodies.
packages/core/src/api/route-utils.ts Public re-export bundle for plugin route handlers (parse/api helpers + finalizeSetup).
packages/core/package.json Exposes new public exports for provider descriptors/admin components + route utils/schemas; adds optional peer for atproto plugin.
packages/auth/src/oauth/consumer.ts Extracts shared findOrCreateOAuthUser() and updates callback path to use it.
packages/auth/src/index.ts Re-exports findOrCreateOAuthUser + CanSelfSignup type.
packages/admin/src/lib/auth-provider-context.tsx Adds React context for provider UI modules and helper hooks (list ordering).
packages/admin/src/lib/api/index.ts Re-exports fetchAuthMode().
packages/admin/src/lib/api/client.ts Adds fetchAuthMode() client for public auth mode endpoint.
packages/admin/src/index.ts Re-exports auth provider context utilities from admin package.
packages/admin/src/components/SetupWizard.tsx Refactors setup wizard to offer passkey + provider methods and show URL error banners.
packages/admin/src/components/LoginPage.tsx Refactors login page to render providers dynamically and fetch auth mode from public endpoint.
packages/admin/src/App.tsx Wires auth provider registry into admin app via context provider.
demos/simple/package.json Adds atproto plugin to demo dependencies.
demos/simple/astro.config.mjs Configures authProviders: [github(), google(), atproto()] in the demo.
.changeset/stale-knives-fix.md Changeset documenting the new pluggable auth provider system and AT Protocol support.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@simnaut simnaut force-pushed the claude/atproto-pds-auth-setup-LWtQo branch from 08c0312 to 173d8ed Compare April 9, 2026 02:32
@simnaut simnaut force-pushed the claude/atproto-pds-auth-setup-LWtQo branch from 173d8ed to d64888e Compare April 9, 2026 02:34
@simnaut
Copy link
Copy Markdown
Author

simnaut commented Apr 9, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
Copy link
Copy Markdown
Collaborator

ascorbic commented Apr 9, 2026

Great! The test failures are just an outdated lockfile. I have a few things I'd like addressing, but I need to dig into it a bit deeper. I'll leave another review in a few hours.

*
* @example ["did:plc:abc123", "did:web:example.com"]
*/
allowedDIDs?: string[];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What's the default if none of these allowlists are set? I think the ideal would be to forbid self-signup in that case, except for the admin set up during onboarding

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The explicit check to prevent sign up when no allowedDIDs and allowedHandles are defined is added in up5f8b24e7d02ecfd956a96f270d5162e62aaf0520.

@@ -0,0 +1,99 @@
/**
* Database-backed store for AT Protocol OAuth state and sessions.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rather than this approach where we're manually handling SQL, could AuthProviderDescriptor include storage like plugins do?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Check out 5f8b24e, let me know if this is what you are looking for!

@github-actions github-actions bot mentioned this pull request Apr 10, 2026
14 tasks
simnaut pushed a commit to simnaut/emdash that referenced this pull request Apr 10, 2026
…rage, update terminology

Addresses all 10 review comments on PR emdash-cms#398:

- Split auth provider into @emdash-cms/auth-atproto (npm-installable),
  keep syndication plugin in @emdash-cms/plugin-atproto (marketplace)
- Add `storage` field to AuthProviderDescriptor, reuse plugin storage
  infrastructure instead of manual SQL table creation
- Rename "AT Protocol" → "Atmosphere", remove "PDS" from user-facing strings
- Forbid self-signup when no allowlists configured (except first admin)
- Fix core callback.ts import to use #db alias
- Fix env.d.ts to reference emdash/locals package

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
simnaut added a commit to simnaut/emdash that referenced this pull request Apr 10, 2026
…rage, update terminology

Addresses all 10 review comments on PR emdash-cms#398:

- Split auth provider into @emdash-cms/auth-atproto (npm-installable),
  keep syndication plugin in @emdash-cms/plugin-atproto (marketplace)
- Add `storage` field to AuthProviderDescriptor, reuse plugin storage
  infrastructure instead of manual SQL table creation
- Rename "AT Protocol" → "Atmosphere", remove "PDS" from user-facing strings
- Forbid self-signup when no allowlists configured (except first admin)
- Fix core callback.ts import to use #db alias
- Fix env.d.ts to reference emdash/locals package
@simnaut simnaut force-pushed the claude/atproto-pds-auth-setup-LWtQo branch from 9213a5b to 2bf86c2 Compare April 10, 2026 03:30
@simnaut simnaut force-pushed the claude/atproto-pds-auth-setup-LWtQo branch from 2bf86c2 to 75c4152 Compare April 10, 2026 03:36
simnaut added a commit to simnaut/emdash that referenced this pull request Apr 10, 2026
…rage, update terminology

Addresses all 10 review comments on PR emdash-cms#398:

- Split auth provider into @emdash-cms/auth-atproto (npm-installable),
  keep syndication plugin in @emdash-cms/plugin-atproto (marketplace)
- Add `storage` field to AuthProviderDescriptor, reuse plugin storage
  infrastructure instead of manual SQL table creation
- Rename "AT Protocol" → "Atmosphere", remove "PDS" from user-facing strings
- Forbid self-signup when no allowlists configured (except first admin)
- Fix core callback.ts import to use #db alias
- Fix env.d.ts to reference emdash/locals package
simnaut and others added 4 commits April 9, 2026 20:55
Introduces a pluggable auth provider system and uses it to add AT Protocol
authentication as the first plugin-based provider. GitHub and Google OAuth
are refactored from hardcoded buttons into the same provider interface.

- AuthProviderDescriptor interface with admin UI, routes, and public routes
- virtual:emdash/auth-providers Vite module for distributing provider components
- Shared findOrCreateOAuthUser() in @emdash-cms/auth for consistent signup gating
- AT Protocol auth via @atcute/oauth-node-client with PKCE (public client)
- allowedDIDs and allowedHandles config with independent handle verification
  via DNS-over-HTTPS + HTTP well-known (never trusts PDS handle claims)
- Default role for new signups changed to Subscriber
- First user becomes Admin during setup regardless of provider
- Replace countUsers() with setup_complete option flag check in both
  GitHub/Google and ATProto OAuth callbacks to prevent concurrent
  callbacks from both claiming first-user admin role
- Memoize ensureTable() in db-store.ts with a module-level boolean
  so CREATE TABLE IF NOT EXISTS only runs once per process
…rage, update terminology

Addresses all 10 review comments on PR emdash-cms#398:

- Split auth provider into @emdash-cms/auth-atproto (npm-installable),
  keep syndication plugin in @emdash-cms/plugin-atproto (marketplace)
- Add `storage` field to AuthProviderDescriptor, reuse plugin storage
  infrastructure instead of manual SQL table creation
- Rename "AT Protocol" → "Atmosphere", remove "PDS" from user-facing strings
- Forbid self-signup when no allowlists configured (except first admin)
- Fix core callback.ts import to use #db alias
- Fix env.d.ts to reference emdash/locals package
@simnaut simnaut force-pushed the claude/atproto-pds-auth-setup-LWtQo branch from 75c4152 to 5f8b24e Compare April 10, 2026 03:56
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.

3 participants