Skip to content

feat: accept invites via OAuth (Google/GitHub), not just passkeys#1868

Open
afonsojramos wants to merge 3 commits into
emdash-cms:mainfrom
afonsojramos:feat/oauth-acceptable-invites
Open

feat: accept invites via OAuth (Google/GitHub), not just passkeys#1868
afonsojramos wants to merge 3 commits into
emdash-cms:mainfrom
afonsojramos:feat/oauth-acceptable-invites

Conversation

@afonsojramos

@afonsojramos afonsojramos commented Jul 7, 2026

Copy link
Copy Markdown

What does this PR do?

Adds OAuth-acceptable invites: an invited user can complete their invite by signing in with a configured OAuth provider (Google/GitHub) instead of only registering a passkey.

Today invites are passkey-only (invite/complete requires a WebAuthn credential), and OAuth only logs in existing users or self-signs-up whole email domains via allowed_domains. There was no way to invite one specific person who then joins via social login without hand-writing a users row. This closes that gap while keeping per-email control.

How it works

  • The invite token is carried through the OAuth state (OAuthState.inviteToken). createAuthorizationUrl accepts an optional inviteToken; the start route reads ?invite=<token> and the state store persists it.
  • In the callback, when the state carries an invite token, handleOAuthCallback delegates to the new exported acceptInviteViaOAuth(...). It validates the invite, requires the provider-verified email to match the invited address (case-insensitive), then completes the invite (creating the user with the invited role) and links the OAuth account. Unverified/mismatched email and invalid/expired tokens are rejected (invite_email_mismatch / invite_invalid).
  • The invite-accept page renders the configured providers under an "Or continue with" divider (LoginButton gained an optional inviteToken prop).

Related discussion: #1859 (filed, awaiting maintainer feedback).

#1720 has merged and this branch is rebased on it. The invite/login buttons keep using the shared provider LoginButton; #1720's normalizeAdminHref now returns "" for /_emdash/api/... paths (covered by normalize-admin-href.test.ts), so the absolute OAuth href is no longer routed through TanStack Router and navigates correctly (same tab). No external prop is used — that would force target="_blank", which is wrong for a login redirect.

Type of change

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

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8

Screenshots / test output

New tests — packages/core/tests/unit/auth/oauth-invite.test.ts (8 tests): matching verified email completes the invite (invited role applied, OAuth account linked, single-use token consumed); case-insensitive email match; mismatched email rejected (no user created); unverified provider email rejected with a distinct code; the invite is consumed when linking to a pre-existing account; the already-linked branch rejects (without consuming) when the linked user's email differs from the invite; invalid/unknown token rejected; errors surface as OAuthError.

 Test Files  1 passed (1)
      Tests  8 passed (8)

Full-repo pnpm typecheck, pnpm lint (oxlint --type-aware --deny-warnings), and pnpm format:check all pass. Existing @emdash-cms/auth (57) and core auth (225) suites still pass.

@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5600977

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

This PR includes changesets to release 16 packages
Name Type
@emdash-cms/auth Minor
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/plugin-embeds 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 github-actions Bot added the review/needs-review No maintainer or bot review yet label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The approach is the right one for EmDash: carrying the invite token through OAuth state lets an invited user accept via Google/GitHub while preserving per-email control and the passkey path. The implementation is generally well-scoped and follows repo conventions — Lingui-wrapped UI strings, RTL-safe Tailwind, no raw SQL, a clear changeset, and unit tests for the success/failure modes of acceptInviteViaOAuth.

I found three issues worth addressing before merge:

  1. Invite-token single-use invariant is not always enforced. When acceptInviteViaOAuth finds an existing user (or OAuth account) for the invite email, it links the account and returns without deleting the invite token. The comment calls this “single-use,” but in this branch the token is left valid. A race with passkey-accept, an admin-created user, or a retried callback can land here, leaving a live invite that can still be validated on the invite-accept page. The fix is simple: delete the token after linking.

  2. New invite-accept OAuth buttons inherit the admin basepath bug. You already call this out in the PR description (#1720 dependency), but it means the UI added here is not actually usable without that fix: Kumo’s LinkProvider in the admin SPA treats the absolute /_emdash/api/auth/oauth/... href as a TanStack Router path and prepends /_emdash/admin, producing the doubled /_emdash/admin/_emdash/api/auth/oauth/... URL. Either fold a LinkProvider fix in here or ensure #1720 lands first.

  3. Unverified provider email gives a misleading message. consumer.ts throws invite_email_mismatch for both “email not verified” and “email does not match,” and callback.ts maps the code to “This invite was sent to a different email address…”. Users with an unverified provider email will get the wrong message. Consider a distinct invite_email_unverified code and mapping.

None of these are security regressions, but #1 is a data-integrity / invariant issue and #2 means the user-facing half of the feature is broken in this branch.

Comment thread packages/auth/src/oauth/consumer.ts
Comment thread packages/admin/src/components/InviteAcceptPage.tsx
@afonsojramos

Copy link
Copy Markdown
Author

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

github-actions Bot added a commit that referenced this pull request Jul 8, 2026
afonsojramos added a commit to afonsojramos/emdash that referenced this pull request Jul 8, 2026
…erified-email error

Addresses review on emdash-cms#1868:
- acceptInviteViaOAuth now deletes the invite token in the existing-user and
  existing-account branches, preserving the single-use guarantee.
- Unverified provider email throws a new invite_email_unverified code with its
  own callback message, instead of the misleading email-mismatch message.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 8, 2026
Copilot AI review requested due to automatic review settings July 8, 2026 07:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds OAuth-based invite acceptance to EmDash so invited users can complete their invite by signing in with a configured OAuth provider (Google/GitHub) rather than being limited to passkey registration. This extends the existing OAuth consumer flow to optionally complete an invite during the callback and updates the admin invite-accept UX to present OAuth options.

Changes:

  • Carry an optional invite token through the OAuth initiation/state store and complete invites during the OAuth callback when present.
  • Add admin UI support for accepting invites via provider login buttons (Google/GitHub) on the invite-accept page.
  • Add unit tests covering invite acceptance via OAuth (including replay protection and email verification/mismatch cases) and publish a changeset.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/core/tests/unit/auth/oauth-invite.test.ts New unit tests validating OAuth-based invite completion behavior and error cases.
packages/core/src/auth/providers/google-admin.tsx Adds inviteToken support to the Google OAuth login button href.
packages/core/src/auth/providers/github-admin.tsx Adds inviteToken support to the GitHub OAuth login button href.
packages/core/src/auth/oauth-state-store.ts Persists/rehydrates inviteToken in stored OAuth state.
packages/core/src/astro/routes/api/auth/oauth/[provider]/callback.ts Maps new invite-related OAuth error codes to user-facing messages.
packages/core/src/astro/routes/api/auth/oauth/[provider].ts Reads ?invite= and passes it into createAuthorizationUrl options.
packages/auth/src/oauth/types.ts Extends OAuthState with optional inviteToken documentation and typing.
packages/auth/src/oauth/consumer.ts Implements acceptInviteViaOAuth and routes invite flows from handleOAuthCallback.
packages/auth/src/index.ts Exports acceptInviteViaOAuth from the auth package public API.
packages/admin/src/lib/auth-provider-context.tsx Updates provider LoginButton typing to accept an optional inviteToken.
packages/admin/src/components/InviteAcceptPage.tsx Renders configured provider buttons (with invite token) under an “Or continue with” divider.
.changeset/oauth-acceptable-invites.md Declares a minor release across affected packages with release notes.

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

Comment thread packages/auth/src/oauth/consumer.ts
Comment thread packages/core/src/astro/routes/api/auth/oauth/[provider].ts
Comment thread packages/core/src/auth/providers/google-admin.tsx
Comment thread packages/core/src/auth/providers/github-admin.tsx
Comment thread .changeset/oauth-acceptable-invites.md Outdated
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 8, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1868

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1868

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1868

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1868

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1868

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1868

emdash

npm i https://pkg.pr.new/emdash@1868

create-emdash

npm i https://pkg.pr.new/create-emdash@1868

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1868

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1868

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1868

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1868

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1868

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1868

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1868

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1868

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1868

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1868

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1868

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1868

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1868

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1868

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1868

commit: 9731675

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the right change for EmDash: carrying the invite token through OAuth state lets invited users accept via Google/GitHub while preserving per-email control and leaving the passkey path intact. The implementation is well-scoped and follows repo conventions — Lingui-wrapped UI strings, RTL-safe Tailwind, no raw SQL, a clear changeset, and focused unit tests for acceptInviteViaOAuth.

Re-checking against the prior review:

  1. Invite-token single-use invariant — fixed. acceptInviteViaOAuth now deletes the invite token in every success path: retried callback (existingAccount), pre-existing user (existingUser), and fresh invite completion (completeInvite). The new test verifies the token cannot be replayed after linking to a pre-existing account.
  2. Unverified-email messaging — fixed. The code now throws a distinct invite_email_unverified OAuthError, and the callback maps it to a user-friendly message.
  3. Admin basepath bug — still open. The OAuth buttons added to the invite-accept page still use Kumo LinkButton without external, so in the admin SPA they get routed through TanStack Router and produce the doubled /_emdash/admin/_emdash/api/auth/oauth/... URL. This is the same inherited issue the PR description already calls out as depending on #1720; I am re-flagging it below because the new invite UI is affected.

I did not find any new logic bugs, SQL-safety issues, authorization gaps, or missing locale filters. The backend invite-completion path is solid and tested.

Comment thread packages/core/src/auth/providers/github-admin.tsx
Comment thread packages/core/src/auth/providers/google-admin.tsx
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 8, 2026
An invited user can complete their invite by signing in with a configured
OAuth provider. The invite token is carried through the OAuth state, and the
callback completes the invite (creating the user with the invited role and
linking the account) only when the provider-verified email matches the invited
address. The invite-accept page renders the configured providers under an
'Or continue with' divider.
…erified-email error

Addresses review on emdash-cms#1868:
- acceptInviteViaOAuth now deletes the invite token in the existing-user and
  existing-account branches, preserving the single-use guarantee.
- Unverified provider email throws a new invite_email_unverified code with its
  own callback message, instead of the misleading email-mismatch message.
…n, changeset

- acceptInviteViaOAuth: in the already-linked branch, require the linked user's
  email to match the invite before consuming, so an identity whose email changed
  after linking can't consume someone else's invite.
- oauth/[provider]: validate the ?invite= token shape/length before persisting
  to the (unauthenticated) OAuth state store.
- changeset: rewrite as a single user-facing sentence.
@afonsojramos afonsojramos force-pushed the feat/oauth-acceptable-invites branch from 9731675 to 5600977 Compare July 9, 2026 21:01
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 9, 2026
@afonsojramos

Copy link
Copy Markdown
Author

@ascorbic rebased after merging #1720

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

Labels

area/admin area/auth area/core bot:review Trigger an emdashbot code review on this PR cla: signed review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants