Skip to content

Unified authentication: a single sign-in surface for portal and admin#284

Merged
mortondev merged 85 commits into
mainfrom
feat/unified-auth
Jun 25, 2026
Merged

Unified authentication: a single sign-in surface for portal and admin#284
mortondev merged 85 commits into
mainfrom
feat/unified-auth

Conversation

@mortondev

@mortondev mortondev commented Jun 23, 2026

Copy link
Copy Markdown
Member

Closes #270 — one sign-in surface for admins and regular users (with an "Admin area" entry in the user dropdown), and a single SSO/sign-in config governing both.

What this does

Consolidates authentication onto a single sign-in surface for the portal and the admin team, and tidies up the Security settings that drive it.

  • One unified sign-in dialog replaces the separate /auth/login and /auth/signup pages. Old login redirects point at it, and the private-portal gate auto-opens it while honouring callbackUrl.
  • Instant-SSO redirect for single-provider, SSO-only workspaces, with hardened callbackUrl validation against open-redirects.
  • Per-provider SSO test sign-in, and the IdP "kind" is now persisted rather than inferred.
  • Recovery codes are required before SSO enforcement can lock out password / magic-link.
  • Team members get an Admin entry in the portal user dropdown.

Security settings (latest commit)

  • Recovery codes now sit inside the Single sign-on (OIDC) card as a subsection, instead of a standalone card.
  • The OIDC card is titled "Single sign-on (OIDC)", with the tier-on and tier-off states unified into one card.
  • "Require two-factor authentication" is nested under Password with a left-rule treatment and a state-aware description.
  • Header, Email, and Social sign-in copy refreshed.

Testing

  • bun run typecheck: clean.
  • bun run test apps/web/src/components/admin/settings/security: 42 tests pass across 7 files.
  • eslint on the changed files: clean.

mortondev added 30 commits June 20, 2026 11:38
…hods

- New script set-portal-auth-methods.ts (disable/restore/enable-magic-link)
  reads and patches settings.portal_config.oauth in the DB, mirroring the
  set-workspace-anon.ts pattern
- Case (d) now disables all portal auth methods before navigating, asserts
  the team form still renders, and restores in a finally block
- Case (c) wraps loginViaMagicLink in enable-magic-link/restore so the
  portal magic-link hook does not silently drop the send on repeat runs
  (existing role='user' principals are blocked by hooks.ts:277-279 when
  portal magicLink is off by default)
- Case (c) now passes { role: 'user' } explicitly to loginViaMagicLink
- Added flushMagicLinkRateLimit helper (execFileSync, no shell) + beforeAll
- Added comment to case (e) confirming /auth/login loader does not resolve
  the invitation id — only isTeamCallback/isSafeCallbackUrl are called
…config domain normalization, per-provider test/enforcement, onboarding registry read)
…, single-source button predicate, non-sso enforce UI dead-end)
Move the recoveryLink element outside the emailEntryEnabled guard in
Stage 1 so it renders whenever showRecoveryLink is true, regardless of
whether password or magic-link is enabled. SSO-only workspaces
(password=false, magicLink=false) previously hid the link entirely
because the emailEntryEnabled block was skipped.

Adds two tests: recovery link absent when callbackUrl is undefined,
and recovery link visible in SSO-only Stage 1 with a team callbackUrl.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

- backfill: materialize legacy team magic-link when password is explicitly
  disabled so passwordless workspaces aren't migrated to zero sign-in methods,
  while preserving an explicit magicLink:false
- backfill: bump authConfigVersion after creating the custom-oidc provider
- config-file: accept and ignore deprecated auth/features keys instead of
  failing the whole watcher; warn and surface the ignored keys in status
- idp: extend the SSRF guard and detailsChangedAt restamp to the manual
  authorization/token/userinfo endpoints, not just discoveryUrl

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

openAuthPopover({ mode: 'login' })

P2 Badge Pass the gate callback when opening sign-in

When a private portal visitor clicks the gate's Sign in / Register CTA (rather than the auto-open path) and then enters an email routed to SSO, the dialog receives no callback and PortalAuthFormInline defaults the same-tab OAuth callback to /. That path does not use the popup broadcast that would keep the user on the gated page, so routed SSO returns them to the root instead of the private URL; pass the safe/current callback here as the auto-open branch does.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/server/functions/instant-sso.ts
Comment thread apps/web/src/lib/server/functions/sso-test.ts Outdated
After sign-in the header goes straight from the Log in / Sign up buttons
to the logged-in avatar once the session refetch lands. Drops the
signingIn bridge, its render branch, and the now-unused ArrowPathIcon.
…plumbing

The auth dialog now handles TOTP challenge and enrollment inline (Task 4);
the standalone /auth/two-factor and /auth/two-factor-setup-required pages
are obsolete. No-op onTwoFactorRedirect so Better-Auth never navigates
away. Delete all related sessionStorage helpers (stash/read/clear/resolve)
and their test file. Remove /auth/two-factor-setup-required from
isTeamCallback's prefix list and update the routing test accordingly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57d509e5a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- idp: reject enabling a provider with no usable OAuth endpoint source
  (require a discovery URL, or both authorization + token URLs) so a
  sole-IdP workspace can't publish a broken login flow
- credentials: enforce the last-working-method invariant before deleting
  provider credentials (the low-level updateAuthConfig bypassed the guard)
- sso-test: request the provider's configured scopes instead of a hardcoded
  set, so a test mirrors production and can't unlock enforcement spuriously
- instant-sso: fall back to the requested deep link (location.pathname) so a
  sole-IdP redirect returns the user to the private route they asked for

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26f2277570

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/server/domains/settings/identity-providers.service.ts Outdated
Providers configured with manual endpoints (authorization + token, no
discovery document — e.g. migrated custom-OIDC) could sign in but the SSO
test rejected them as not-configured, so they could never be tested and
therefore never enforced (Codex P2).

- add jwks_uri + issuer columns to identity_provider (migration 0117) so a
  discovery-less provider carries what the test needs to verify the ID token
- thread jwksUri/issuer through the service type, zod input, SSRF guard
  (jwks_uri is server-fetched), and the detailsChangedAt restamp
- startSsoTestFn + the handshake now resolve endpoints from the discovery doc
  OR the stored manual endpoints; discovery providers are unchanged
- provider editor gains a collapsible "Manual endpoints" section for the
  Other kind (authorization/token/userinfo/jwks/issuer)
- _portal loader: optional-chain location.pathname so a missing location
  (e.g. in unit tests) can't crash the portal-access path
- sso-test-callback test: assert the manual-endpoint fields now threaded to
  runHandshake (tokenEndpoint / jwksUri / issuer)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7d09cd574

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/server/functions/sso.ts Outdated
Comment thread apps/web/src/lib/server/auth/sso-test-callback.ts Outdated
- idp: registrationId is immutable on update — keep the stored id rather than
  the caller's, so an edit can't orphan the secret/accounts/redirect URI (or
  slip past the enabled-based last-method guard)
- idp: restrict registrationId to the oidc_ namespace (+ legacy sso/custom-oidc)
  so a provider can't register under a built-in method id like `credential`
  and bypass a disabled built-in toggle via the registered-OIDC short-circuit
- sso-test: capture the provider's detailsChangedAt in the test session and
  only stamp lastSuccessfulTestAt when it's unchanged at callback time, so a
  mid-test edit can't let a stale test unlock enforcement for a new config

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 844d1804a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/components/auth/portal-auth-form-inline.tsx
Comment thread apps/web/src/components/admin/settings/security/auth-method-count.ts Outdated
- auth dialog: closing mid-2FA (X / backdrop / Esc) now revokes the session
  signIn.email created, not just the in-form Cancel button. Without it a
  required-2FA user who never finishes enrollment kept a valid, un-enrolled
  session and bypassed the policy on the next navigation. Success closes
  programmatically (no onOpenChange), so this only fires on a real abandon.
- auth-method-count: seed the count with the default-on password (absent
  oauth.password means on), so the settings UI no longer undercounts methods
  and wrongly locks the Remove/Off control on the last IdP/social provider.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

The CI `test` job intermittently failed with an unhandled "window is not
defined": a setState fired after the test env was torn down.

- two-factor-enroll-steps: re-check the unmount flag after the QR await so a
  late-resolving enable/QR chain can't setState on an unmounted component
- enroll-steps test: stub the OTP input (as the sibling dialog test does) —
  the real input-otp schedules effects/RAF that outlive the test and trip the
  teardown race; a plain controlled input keeps the run deterministic
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Post-review cleanup of the unified-auth changes. No behavior change
except the deliberate decision that config-file can no longer lock the
auth/feature toggles (that management was already removed upstream).

- drop dead markSsoDetailsChanged + its stale test mock
- remove 6 orphaned portal.auth.* i18n keys across all locales
- remove the unused docUrl field from idp-shortcuts
- unify the duplicated settings-oauth JSON parser across both backfills
- share DEFAULT_OIDC_SCOPES between production and the SSO test flow
- remove vestigial "managed by config" badges/disabled-state on the
  sign-in providers and Labs tabs (config no longer manages these)
- stop the login dispatcher fetching configured types twice
- dedupe the 6-slot OTP group, isOnlyMethod, and the _portal auth-prompt
  parse; inline a single-use redirect helper; hoist a duplicated import
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Replace the private-portal gate's intermediate "Sign in / Register" card and
modal with the shared auth form rendered directly as a dedicated sign-in
screen, so visitors land straight on it instead of clicking through.

- Extract headerForStep into auth-step-header.tsx with a `surface` option.
  The private gate uses private-portal framing copy ("Sign in to access X" /
  "This portal is private...") instead of the public "vote and comment"
  tagline; the public dialog is unchanged.
- The gate seeds the form mode from ?auth=signin|signup and keeps the
  post-sign-in broadcast-to-loader-rerun bridge (now swapping the whole form
  for a "Signing in..." state).
- Preserve the 2FA-abandon session revoke: the modal does it on close, the
  inline form revokes on unmount while mid-2FA (skipped once sign-in succeeds).
- Replace the gate's skeleton backdrop with a blurred faux feedback board
  built from hardcoded mock data. No real content reaches the browser and
  every element is inert (aria-hidden, nothing focusable).

Tests cover the inline render, mode seeding, the signing-in bridge, the
2FA-abandon revoke, and the backdrop inertness invariant.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@mortondev mortondev merged commit 9b2d28c into main Jun 25, 2026
7 checks passed
@mortondev mortondev deleted the feat/unified-auth branch June 25, 2026 07:35
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.

Single SSO provider for both admin and regular users

2 participants