Skip to content

Add OIDC SSO support#136

Merged
JoachimLK merged 6 commits intomainfrom
feat/sso
Apr 10, 2026
Merged

Add OIDC SSO support#136
JoachimLK merged 6 commits intomainfrom
feat/sso

Conversation

@JoachimLK
Copy link
Copy Markdown
Contributor

@JoachimLK JoachimLK commented Apr 9, 2026

Summary

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

  • New Features
    • OIDC Single Sign-On: optional env-driven toggle, configurable provider label, SSO buttons on sign-in/sign-up, org-scoped provider management UI with add/delete and copyable callback URLs, mobile/sidebar settings entries, redirecting/loading states and improved error messaging.
  • Backend
    • New SSO APIs, DB table for providers, server-side validation, provisioning and permission checks, trusted-origin handling, and PKCE/issuer validation enforced.
  • Documentation
    • SELF-HOSTING updated with OIDC setup, provider examples, restart and security notes.
  • Tests
    • Extensive new unit/integration tests covering env, API schemas, security, and integration readiness.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds OpenID Connect (OIDC) Single Sign-On: docs and .env entries, runtime flags, frontend sign-in/sign-up SSO buttons and handlers, auth-client plugin registration, backend global and per-organization SSO config, trusted-origin adjustments, DB migration/schema for sso_provider, provider CRUD API endpoints, admin UI for provider management, and extensive tests and validation.

Changes

Cohort / File(s) Summary
Config & Docs
\.env.example, SELF-HOSTING.md
Add optional OIDC env vars (OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_DISCOVERY_URL, OIDC_PROVIDER_NAME), provider discovery examples, redirect/redirect-URI guidance, PKCE/issuer notes, and restart instructions.
Frontend Auth Pages
app/pages/auth/sign-in.vue, app/pages/auth/sign-up.vue
Add oidcEnabled/oidcProviderName runtime flags, SSO UI/buttons and handlers (handleSelfHostedSso, handleEnterpriseSso, handleSsoSignUp), improve email auth validation/error handling, telemetry events, and redirect logic.
Auth Client & Dependencies
app/utils/auth-client.ts, package.json
Register genericOAuthClient() and ssoClient() in auth client plugins; add @better-auth/sso and bump better-auth to ^1.6.1.
Runtime Config
nuxt.config.ts
Expose runtimeConfig.public.oidcEnabled (true when core OIDC envs set) and oidcProviderName (default "SSO").
Backend Auth & Trusted Origins
server/utils/auth.ts
Make resolveTrustedOrigins request-aware for SSO routes, add conditional global genericOAuth and per-org sso() wiring (PKCE, issuer validation), and add profile→user mapping and provisioning sync.
Env Validation
server/utils/env.ts
Export envSchema, add OIDC env entries with URL validation and default OIDC_PROVIDER_NAME, and enforce all-or-none OIDC presence via .superRefine.
SSO Provider API
server/api/sso/providers.get.ts, server/api/sso/providers.post.ts, server/api/sso/providers/[id].delete.ts
Add org-scoped SSO provider CRUD endpoints with permission checks, Zod validation, uniqueness checks, auth.api.registerSSOProvider integration, error mapping, and guarded deletion.
SSO Management UI & Nav
app/pages/dashboard/settings/sso.vue, app/components/SettingsSidebar.vue, app/components/SettingsMobileNav.vue
New SSO settings page (list/register/delete), copyable callback URIs, quick setup guide, permission gating; add settings nav entries with ShieldCheck icon and Beta badge.
Database & Schema
server/database/migrations/0019_noisy_strong_guy.sql, server/database/migrations/meta/_journal.json, server/database/schema/sso.ts, server/database/schema/index.ts
Add sso_provider migration and journal entry, Drizzle ssoProvider schema, relations, indexes, and re-export in schema index.
Tests — Env, Schema, API & Security
tests/unit/* (many new files)
Add extensive Vitest suites covering env schema, discovery URL validation, trusted origins, provider API validation, auth mapping, provisioning intent, security edge cases, and integration-readiness checks.
Minor Frontend/Config Formatting
app/components/..., nuxt.config.ts
Small UI tweaks (badges/labels), icon import, and various string/formatting adjustments across config and components.

Sequence Diagram(s)

sequenceDiagram
    participant User as Browser User
    participant FE as Frontend (Auth Page)
    participant AC as Auth Client
    participant BE as Backend (App Server)
    participant IdP as Identity Provider

    User->>FE: Click "Sign in with SSO"
    FE->>AC: authClient.signIn.oauth2({ providerId: "oidc", callbackURL })
    AC->>BE: Initiate OIDC auth flow (redirect / authorize)
    BE->>IdP: Discovery + Authorization Request (issuer validation, PKCE)
    IdP-->>User: Redirect to IdP login
    User->>IdP: Authenticate
    IdP-->>FE: Redirect back with code (callback URL)
    FE->>BE: Callback request with code
    BE->>IdP: Exchange code for tokens, validate issuer
    IdP-->>BE: ID token + profile
    BE->>BE: mapProfileToUser() → create/update user & session
    BE-->>FE: Redirect to dashboard with session cookie
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through envs and provider trees,
I stitched callbacks and PKCE with ease,
A button gleams and redirects sing,
New SSO paths make users spring,
🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add OIDC SSO support' clearly and concisely summarizes the main change: implementing OIDC-based Single Sign-On functionality across the application.
Description check ✅ Passed The PR description addresses the main requirement (Summary section with what/why), marks feature type, and indicates local testing was done. However, it does not check the documentation update or multi-tenant auth verification checkboxes, and DCO sign-off remains unchecked.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sso

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@railway-app
Copy link
Copy Markdown

railway-app Bot commented Apr 9, 2026

🚅 Deployed to the reqcore-pr-136 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Apr 10, 2026 at 10:55 am

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-136 April 9, 2026 08:10 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
nuxt.config.ts (1)

216-223: Minor inconsistency in OIDC enablement check vs. env schema.

The oidcEnabled check uses raw process.env truthiness, while server/utils/env.ts uses emptyToUndefined preprocessing that converts whitespace-only strings (e.g., " ") to undefined.

If someone sets OIDC_CLIENT_ID=" " (whitespace only), this config would evaluate oidcEnabled: true (whitespace string is truthy), but the server-side env validation would reject it, causing the SSO button to appear but fail at runtime.

This is an unlikely edge case, but for consistency you could trim the values:

♻️ Optional: Align with server-side preprocessing
       /** Whether OIDC SSO is enabled (all three OIDC env vars are set) */
       oidcEnabled: !!(
-        process.env.OIDC_CLIENT_ID &&
-        process.env.OIDC_CLIENT_SECRET &&
-        process.env.OIDC_DISCOVERY_URL
+        process.env.OIDC_CLIENT_ID?.trim() &&
+        process.env.OIDC_CLIENT_SECRET?.trim() &&
+        process.env.OIDC_DISCOVERY_URL?.trim()
       ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nuxt.config.ts` around lines 216 - 223, The oidcEnabled check in
nuxt.config.ts uses raw process.env truthiness which treats whitespace-only
values as truthy; update the oidcEnabled computation to mirror the server-side
preprocessing (e.g., use the same emptyToUndefined logic or trim+validate the
three vars) so that OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_DISCOVERY_URL
that are whitespace-only evaluate as undefined/false; reference the oidcEnabled
property and the process.env keys and align with server/utils/env.ts behavior
(or call the same helper) to ensure consistency with server-side validation and
prevent the SSO button showing for invalid env values.
server/utils/env.ts (1)

117-118: Enforce OIDC discovery URL format in schema validation.

The comment correctly states that OIDC_DISCOVERY_URL must point to a .well-known/openid-configuration endpoint, which aligns with better-auth's genericOAuth documentation. However, the schema validation only checks generic URL format with z.string().url(), allowing misconfigured values like https://example.com or http://insecure.url to pass validation but fail at runtime.

Add validation to enforce:

  • HTTPS protocol (required for OIDC)
  • The .well-known/openid-configuration endpoint path (or at least reject URLs missing it)

This prevents misconfiguration from reaching runtime where better-auth would reject it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/env.ts` around lines 117 - 118, The OIDC_DISCOVERY_URL schema
currently allows any valid URL but must enforce HTTPS and the
.well-known/openid-configuration path; update the OIDC_DISCOVERY_URL definition
(the symbol named OIDC_DISCOVERY_URL that uses
emptyToUndefined.pipe(z.string().url()).optional()) to add a zod refinement that
ensures the URL's protocol is "https:" and that the pathname ends with or
contains ".well-known/openid-configuration" (or rejects values missing that
segment), preserving optional() and emptyToUndefined behavior so invalid configs
fail validation early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/auth/sign-in.vue`:
- Around line 78-93: handleSsoSignIn currently hardcodes callbackURL to
localePath("/dashboard") and therefore drops any invitation context; change it
to read the invitation token from route.query.invitation (same source used by
handleSignIn) and, when present, set callbackURL to the invitation-accepting
route (e.g., localePath("/accept-invitation") or the same path used by
handleSignIn) including the invitation query param so SSO redirects back to the
invitation flow after auth; otherwise fall back to localePath("/dashboard").
Update the call in authClient.signIn.oauth2 to use this conditional callbackURL
and keep existing error/isLoading handling in handleSsoSignIn.

In `@app/pages/auth/sign-up.vue`:
- Around line 92-107: handleSsoSignUp doesn't include the pendingInvitation
context when calling authClient.signIn.oauth2, so SSO users lose their
invitation; update handleSsoSignUp to build the callbackURL using the existing
pendingInvitation (same param used in the email flow)—e.g. if pendingInvitation
is present set callbackURL to
localePath(`/auth/accept-invitation/${pendingInvitation}`) otherwise fallback to
localePath("/dashboard")—and pass that callbackURL into authClient.signIn.oauth2
to preserve invitation context.

In `@server/utils/auth.ts`:
- Around line 126-138: The mapProfileToUser function currently assigns
profile.email directly which can be undefined; update mapProfileToUser to
validate and normalize the email (e.g., check profile.email is a non-empty
string and optionally fallback to profile.preferred_username or null) and ensure
downstream code expects a nullable email. Specifically modify the async
mapProfileToUser(profile) implementation to perform a defensive check on
profile.email, sanitize/trim it, and return a stable value (empty string or
null) when absent so callers of mapProfileToUser (user creation/login code)
won't receive undefined.

In `@server/utils/env.ts`:
- Around line 8-11: The emptyToUndefined helper currently uses z.preprocess with
a fixed inner schema, causing `.optional()` placed outside to not short-circuit
when preprocess returns undefined; refactor emptyToUndefined into a function
that accepts a zod schema (e.g., const emptyToUndefined = (schema) =>
z.preprocess((val) => (typeof val === "string" && val.trim() === "" ? undefined
: val), schema)), then update every schema usage (examples: BETTER_AUTH_SECRET,
RAILWAY_GIT_PR_NUMBER and other entries using emptyToUndefined or
z.preprocess(...).optional()) so that optionality is applied to the inner schema
argument (e.g., pass z.string().optional() or z.string().min(...)) rather than
calling .optional() on the preprocess wrapper.

---

Nitpick comments:
In `@nuxt.config.ts`:
- Around line 216-223: The oidcEnabled check in nuxt.config.ts uses raw
process.env truthiness which treats whitespace-only values as truthy; update the
oidcEnabled computation to mirror the server-side preprocessing (e.g., use the
same emptyToUndefined logic or trim+validate the three vars) so that
OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_DISCOVERY_URL that are
whitespace-only evaluate as undefined/false; reference the oidcEnabled property
and the process.env keys and align with server/utils/env.ts behavior (or call
the same helper) to ensure consistency with server-side validation and prevent
the SSO button showing for invalid env values.

In `@server/utils/env.ts`:
- Around line 117-118: The OIDC_DISCOVERY_URL schema currently allows any valid
URL but must enforce HTTPS and the .well-known/openid-configuration path; update
the OIDC_DISCOVERY_URL definition (the symbol named OIDC_DISCOVERY_URL that uses
emptyToUndefined.pipe(z.string().url()).optional()) to add a zod refinement that
ensures the URL's protocol is "https:" and that the pathname ends with or
contains ".well-known/openid-configuration" (or rejects values missing that
segment), preserving optional() and emptyToUndefined behavior so invalid configs
fail validation early.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8288327e-2b25-4670-8e45-a07dec707ff7

📥 Commits

Reviewing files that changed from the base of the PR and between 596e6c8 and 1b23af3.

📒 Files selected for processing (9)
  • .env.example
  • SELF-HOSTING.md
  • app/pages/auth/sign-in.vue
  • app/pages/auth/sign-up.vue
  • app/utils/auth-client.ts
  • nuxt.config.ts
  • server/utils/auth.ts
  • server/utils/env.ts
  • tests/unit/oidc-sso.test.ts

Comment thread app/pages/auth/sign-in.vue Outdated
Comment thread app/pages/auth/sign-up.vue
Comment thread server/utils/auth.ts
Comment thread server/utils/env.ts
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-136 April 10, 2026 06:41 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (2)
app/pages/auth/sign-in.vue (1)

95-110: ⚠️ Potential issue | 🟠 Major

SSO sign-in doesn't preserve pending invitation context.

Both handleSelfHostedSso and handleEnterpriseSso hardcode callbackURL to /dashboard, losing any pending invitation from route.query.invitation. Users arriving via invitation links would be redirected to the dashboard instead of accepting their invitation after SSO.

🔧 Proposed fix to preserve invitation context
 async function handleSelfHostedSso() {
     isLoading.value = true;
     error.value = "";
+    const pendingInvitation = route.query.invitation as string | undefined;
+    const callbackURL = pendingInvitation
+        ? localePath(`/auth/accept-invitation/${pendingInvitation}`)
+        : localePath("/dashboard");
     try {
         await authClient.signIn.oauth2({
             providerId: "oidc",
-            callbackURL: localePath("/dashboard"),
+            callbackURL,
         });

Apply the same pattern to handleEnterpriseSso:

 async function handleEnterpriseSso() {
     // ... validation ...
     ssoRedirecting.value = true;
     error.value = "";
+    const pendingInvitation = route.query.invitation as string | undefined;
+    const callbackURL = pendingInvitation
+        ? localePath(`/auth/accept-invitation/${pendingInvitation}`)
+        : localePath("/dashboard");

     try {
         const result = await authClient.signIn.sso({
             email: email.value,
-            callbackURL: localePath("/dashboard"),
+            callbackURL,
             errorCallbackURL: localePath("/auth/sign-in"),
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/sign-in.vue` around lines 95 - 110, handleSelfHostedSso (and
likewise handleEnterpriseSso) hardcode callbackURL to localePath("/dashboard")
which drops any pending invitation in route.query.invitation; update both
functions to build the callbackURL from localePath("/dashboard") plus the
invitation query when present (e.g., append ?invitation=<encoded-value> or add
it to existing query string), reading route.query.invitation and only appending
when defined/nonnull, and ensure the value is URL-encoded so the
authClient.signIn.oauth2 call preserves invitation context through the SSO flow.
server/utils/auth.ts (1)

162-174: ⚠️ Potential issue | 🟡 Minor

Missing email validation in profile mapping.

The mapProfileToUser function uses profile.email without verifying it exists. Some OIDC providers may not return an email if unverified or if the scope wasn't granted, potentially creating users with undefined email addresses.

🛡️ Proposed defensive check
                     async mapProfileToUser(profile) {
+                      if (!profile.email) {
+                        throw new Error(
+                          "Email is required but was not provided by the identity provider. Ensure the 'email' scope is granted and the user has a verified email.",
+                        );
+                      }
                       return {
                         name:
                           profile.name ||
                           [profile.given_name, profile.family_name]
                             .filter(Boolean)
                             .join(" ") ||
                           profile.preferred_username ||
                           profile.email,
                         email: profile.email,
                         image: profile.picture,
                       };
                     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/auth.ts` around lines 162 - 174, The mapProfileToUser mapping
currently uses profile.email without validation; update the async function
mapProfileToUser to first verify profile.email exists (and optionally
profile.email_verified if present) and handle the missing/unverified case
instead of returning undefined email—for example, return null (or throw a clear
Error) when email is absent/unverified so the caller can stop user creation, and
otherwise return the existing object (keep the name/image fallbacks intact);
reference profile.email and profile.email_verified in the validation logic
inside mapProfileToUser.
🧹 Nitpick comments (1)
app/pages/dashboard/settings/sso.vue (1)

133-146: Consider guarding window access and improving error handling.

While getCallbackUrl is only called from click handlers (client-side), adding a guard improves robustness. The empty catch block in copyCallbackUrl silently swallows errors without user feedback.

♻️ Suggested improvements
 function getCallbackUrl(providerId: string) {
+  if (!import.meta.client) return ''
   const base = window.location.origin
   return `${base}/api/auth/sso/callback/${providerId}`
 }
 
 async function copyCallbackUrl(providerId: string) {
   try {
     await navigator.clipboard.writeText(getCallbackUrl(providerId))
     copiedProviderId.value = providerId
     setTimeout(() => { copiedProviderId.value = null }, 2000)
   } catch {
-    // Fallback for non-HTTPS environments
+    // Clipboard API unavailable (non-HTTPS or denied permission)
+    formError.value = 'Could not copy to clipboard. Please copy the URL manually.'
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/settings/sso.vue` around lines 133 - 146, getCallbackUrl
currently accesses window unguarded and copyCallbackUrl swallows errors; update
getCallbackUrl to check for typeof window !== 'undefined' (or use optional
chaining on window.location.origin) and fall back to a relative path (e.g.,
`/api/auth/sso/callback/${providerId}`) when origin is unavailable, and update
copyCallbackUrl to catch the error into a variable, log it
(console.error/processLogger), and provide user feedback (set copiedProviderId
appropriately or surface an error message) and optionally attempt a DOM-based
clipboard fallback if navigator.clipboard fails; reference getCallbackUrl and
copyCallbackUrl and the reactive copiedProviderId.value when making these
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/dashboard/settings/sso.vue`:
- Line 420: The template references window directly which causes a
TypeScript/Vue error; add a computed property named appOrigin in the <script
setup> that returns window.location.origin when running on client (use
import.meta.client) and returns the fallback 'https://app.reqcore.com' on
server, then replace the inline typeof window check in the template (the
Redirect URI template expression) with appOrigin so the template no longer
references window directly.

In `@server/api/sso/providers.get.ts`:
- Around line 9-19: The code reimplements org authorization but doesn't check
for a missing active organization; ensure you validate
session.session.activeOrganizationId after calling requireAuth and before
calling (auth.api as any).hasPermission: if activeOrganizationId is
null/undefined/empty, throw createError({ statusCode: 403, statusMessage:
'Forbidden: no active organization' }) (matching the behavior in
requirePermission), otherwise pass the org id into the permission check so
org-scoped queries use a valid id; update the block around requireAuth/session
and the hasPermission call in providers.get.ts to perform this explicit
active-org check.

In `@server/api/sso/providers.post.ts`:
- Around line 20-21: Check for a missing active organization before calling
registerSSOProvider: after obtaining session via requireAuth(event) and reading
session.session.activeOrganizationId into orgId, validate that orgId is present
(not null/undefined) and if missing return or throw an appropriate HTTP error
(e.g., 400/401) with a clear message instead of calling registerSSOProvider;
update the flow that calls registerSSOProvider to only proceed when orgId is
truthy so registration always has proper organization context.

In `@server/api/sso/providers/`[id].delete.ts:
- Around line 14-24: The code currently reads
session.session.activeOrganizationId and runs a manual (auth.api as
any).hasPermission check which misses the explicit "No active organization"
failure; replace this manual logic with the shared requirePermission helper so
the active organization presence and permission check are enforced consistently:
call requirePermission(event, { permissions: { organization: ['update'] } }) (or
the helper's actual signature), use the returned activeOrganizationId instead of
session.session.activeOrganizationId, and remove the existing
auth.api.hasPermission block and its custom 403 throw to rely on
requirePermission's error handling.
- Around line 28-42: Replace the two-step select-then-delete TOCTOU pattern
around ssoProvider with a single atomic delete that returns the deleted row;
call db.delete(ssoProvider).where(and(eq(ssoProvider.id, id),
eq(ssoProvider.organizationId, orgId))).returning(...) and check the returned
rows — if none were returned, throw createError({ statusCode: 404,
statusMessage: 'SSO provider not found' }); otherwise treat it as success;
remove the initial select and ensure you reference ssoProvider and orgId in the
single delete call.

In `@server/database/schema/sso.ts`:
- Around line 19-21: Change the SSO schema field organizationId to be mandatory
by making the column definition organizationId:
text('organization_id').notNull().references(() => organization.id, { onDelete:
'cascade' }) in server/database/schema/sso.ts, then update the migration
0019_noisy_strong_guy.sql to apply the NOT NULL constraint (e.g. ALTER TABLE ...
ALTER COLUMN organization_id SET NOT NULL) and include any necessary data
migration steps (backfill null organization_id rows to a valid org or
delete/abort if appropriate) so the DB constraint and existing data stay
consistent with the new not-null requirement.

---

Duplicate comments:
In `@app/pages/auth/sign-in.vue`:
- Around line 95-110: handleSelfHostedSso (and likewise handleEnterpriseSso)
hardcode callbackURL to localePath("/dashboard") which drops any pending
invitation in route.query.invitation; update both functions to build the
callbackURL from localePath("/dashboard") plus the invitation query when present
(e.g., append ?invitation=<encoded-value> or add it to existing query string),
reading route.query.invitation and only appending when defined/nonnull, and
ensure the value is URL-encoded so the authClient.signIn.oauth2 call preserves
invitation context through the SSO flow.

In `@server/utils/auth.ts`:
- Around line 162-174: The mapProfileToUser mapping currently uses profile.email
without validation; update the async function mapProfileToUser to first verify
profile.email exists (and optionally profile.email_verified if present) and
handle the missing/unverified case instead of returning undefined email—for
example, return null (or throw a clear Error) when email is absent/unverified so
the caller can stop user creation, and otherwise return the existing object
(keep the name/image fallbacks intact); reference profile.email and
profile.email_verified in the validation logic inside mapProfileToUser.

---

Nitpick comments:
In `@app/pages/dashboard/settings/sso.vue`:
- Around line 133-146: getCallbackUrl currently accesses window unguarded and
copyCallbackUrl swallows errors; update getCallbackUrl to check for typeof
window !== 'undefined' (or use optional chaining on window.location.origin) and
fall back to a relative path (e.g., `/api/auth/sso/callback/${providerId}`) when
origin is unavailable, and update copyCallbackUrl to catch the error into a
variable, log it (console.error/processLogger), and provide user feedback (set
copiedProviderId appropriately or surface an error message) and optionally
attempt a DOM-based clipboard fallback if navigator.clipboard fails; reference
getCallbackUrl and copyCallbackUrl and the reactive copiedProviderId.value when
making these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28c2fd3d-13b4-43ca-8c38-6ef59b7dacb5

📥 Commits

Reviewing files that changed from the base of the PR and between 1b23af3 and 62fdf39.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • app/components/SettingsMobileNav.vue
  • app/components/SettingsSidebar.vue
  • app/pages/auth/sign-in.vue
  • app/pages/dashboard/settings/sso.vue
  • app/utils/auth-client.ts
  • package.json
  • server/api/sso/providers.get.ts
  • server/api/sso/providers.post.ts
  • server/api/sso/providers/[id].delete.ts
  • server/database/migrations/0019_noisy_strong_guy.sql
  • server/database/migrations/meta/0019_snapshot.json
  • server/database/migrations/meta/_journal.json
  • server/database/schema/index.ts
  • server/database/schema/sso.ts
  • server/utils/auth.ts
✅ Files skipped from review due to trivial changes (1)
  • server/database/schema/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/utils/auth-client.ts

Comment thread app/pages/dashboard/settings/sso.vue Outdated
Comment thread server/api/sso/providers.get.ts Outdated
Comment thread server/api/sso/providers.post.ts Outdated
Comment thread server/api/sso/providers/[id].delete.ts Outdated
Comment thread server/api/sso/providers/[id].delete.ts Outdated
Comment on lines +19 to +21
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
organizationId: text('organization_id').references(() => organization.id, { onDelete: 'cascade' }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

organizationId should be mandatory for org-scoped SSO providers.

Every new provider endpoint in this PR scopes records by active organization. Leaving Line 21 nullable means you can persist SSO providers that the GET/DELETE API can never see again, which is a bad fit for a tenant-owned auth resource.

Proposed fix
-  organizationId: text('organization_id').references(() => organization.id, { onDelete: 'cascade' }),
+  organizationId: text('organization_id').notNull().references(() => organization.id, { onDelete: 'cascade' }),

Please regenerate or update server/database/migrations/0019_noisy_strong_guy.sql to keep the DB constraint aligned.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
organizationId: text('organization_id').references(() => organization.id, { onDelete: 'cascade' }),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
organizationId: text('organization_id').notNull().references(() => organization.id, { onDelete: 'cascade' }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/database/schema/sso.ts` around lines 19 - 21, Change the SSO schema
field organizationId to be mandatory by making the column definition
organizationId: text('organization_id').notNull().references(() =>
organization.id, { onDelete: 'cascade' }) in server/database/schema/sso.ts, then
update the migration 0019_noisy_strong_guy.sql to apply the NOT NULL constraint
(e.g. ALTER TABLE ... ALTER COLUMN organization_id SET NOT NULL) and include any
necessary data migration steps (backfill null organization_id rows to a valid
org or delete/abort if appropriate) so the DB constraint and existing data stay
consistent with the new not-null requirement.

- Introduced unit tests for SSO integration readiness, validating environment variables and OIDC configurations.
- Implemented tests for SSO provider API validation, ensuring input sanitization and domain validation.
- Added security-focused tests for SSO, covering domain uniqueness, provider ID uniqueness, and response shape to prevent secret leakage.
- Developed tests for trusted origins resolver logic, verifying URL parsing and filtering for SSO callback flows.
- Enhanced input sanitization edge cases to prevent injection attacks and ensure robust validation.
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-136 April 10, 2026 10:17 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
server/utils/env.ts (1)

114-123: ⚠️ Potential issue | 🟠 Major

The emptyToUndefined.pipe(...).optional() pattern may reject empty strings instead of treating them as unset.

When emptyToUndefined preprocesses "" to undefined, the piped inner schema (z.string().min(1)) receives undefined and fails validation—.optional() on the outer layer doesn't help because it only skips validation when the original input is undefined.

For Railway deployments where env vars may be set to "", this could cause unexpected validation failures.

Recommended refactor

Move .optional() inside the pipe so the inner schema accepts undefined:

-OIDC_CLIENT_ID: emptyToUndefined.pipe(z.string().min(1)).optional(),
-OIDC_CLIENT_SECRET: emptyToUndefined.pipe(z.string().min(1)).optional(),
-OIDC_DISCOVERY_URL: emptyToUndefined.pipe(
-  z.string().url().refine(
-    (url) => url.startsWith('https://') || url.startsWith('http://'),
-    'OIDC_DISCOVERY_URL must use HTTPS (or HTTP for local development)',
-  ),
-).optional(),
+OIDC_CLIENT_ID: emptyToUndefined.pipe(z.string().min(1).optional()),
+OIDC_CLIENT_SECRET: emptyToUndefined.pipe(z.string().min(1).optional()),
+OIDC_DISCOVERY_URL: emptyToUndefined.pipe(
+  z.string().url().refine(
+    (url) => url.startsWith('https://') || url.startsWith('http://'),
+    'OIDC_DISCOVERY_URL must use HTTPS (or HTTP for local development)',
+  ).optional(),
+),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/env.ts` around lines 114 - 123, The env schema uses
emptyToUndefined.pipe(z.string().min(1)).optional(), which still fails when
emptyToUndefined turns "" into undefined because the inner schema rejects
undefined; update the three entries (OIDC_CLIENT_ID, OIDC_CLIENT_SECRET,
OIDC_DISCOVERY_URL) to move .optional() inside the pipe so the inner schema
accepts undefined (e.g., emptyToUndefined.pipe(z.string().min(1).optional()) for
the first two and emptyToUndefined.pipe(z.string().url().refine(...).optional())
for OIDC_DISCOVERY_URL), ensuring empty strings are treated as unset while
preserving the existing validations.
app/pages/dashboard/settings/sso.vue (1)

425-425: ⚠️ Potential issue | 🔴 Critical

Fix TypeScript error: window is not available in Vue template context.

The pipeline is failing because Vue templates cannot access window directly. The typeof window !== 'undefined' guard doesn't help since the template compiler still sees window as an undefined property.

Proposed fix using a computed property

Add this computed property in the <script setup> section:

const appOrigin = computed(() => {
  if (import.meta.client) {
    return window.location.origin
  }
  return 'https://app.reqcore.com'
})

Then update line 425:

-<li>Set the <strong>Redirect URI</strong> to: <code class="bg-surface-100 dark:bg-surface-800 px-1 py-0.5 rounded text-xs">{{ `${typeof window !== 'undefined' ? window.location.origin : 'https://app.reqcore.com'}/api/auth/sso/callback/{provider-id}` }}</code></li>
+<li>Set the <strong>Redirect URI</strong> to: <code class="bg-surface-100 dark:bg-surface-800 px-1 py-0.5 rounded text-xs">{{ `${appOrigin}/api/auth/sso/callback/{provider-id}` }}</code></li>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/settings/sso.vue` at line 425, The template is
referencing window directly which the Vue compiler treats as an undefined
template property; add a computed property named appOrigin in the <script setup>
that returns window.location.origin when running client-side (use
import.meta.client) and returns the fallback 'https://app.reqcore.com' for SSR,
then replace the inline window expression in the template with the appOrigin
computed value (use `${appOrigin}/api/auth/sso/callback/{provider-id}`) so the
template no longer references window directly.
app/pages/auth/sign-in.vue (1)

95-103: ⚠️ Potential issue | 🟠 Major

SSO flows still drop invitation context on redirect.

Line 101 and Line 129 always send users to /dashboard, unlike handleSignIn which preserves route.query.invitation. This breaks invite acceptance after SSO. Also preserve invitation on errorCallbackURL so failed SSO returns to the same invite-aware page.

🔧 Suggested fix
 async function handleSelfHostedSso() {
     isLoading.value = true;
     error.value = "";
+    const pendingInvitation = route.query.invitation as string | undefined;
+    const callbackURL = pendingInvitation
+        ? localePath(`/auth/accept-invitation/${pendingInvitation}`)
+        : localePath("/dashboard");
     try {
         await authClient.signIn.oauth2({
             providerId: "oidc",
-            callbackURL: localePath("/dashboard"),
+            callbackURL,
         });
     } catch (e: unknown) {
 async function handleEnterpriseSso() {
@@
+    const pendingInvitation = route.query.invitation as string | undefined;
+    const callbackURL = pendingInvitation
+        ? localePath(`/auth/accept-invitation/${pendingInvitation}`)
+        : localePath("/dashboard");
+    const errorCallbackURL = pendingInvitation
+        ? localePath(`/auth/sign-in?invitation=${encodeURIComponent(pendingInvitation)}`)
+        : localePath("/auth/sign-in");
+
     try {
         const result = await authClient.signIn.sso({
             email: email.value,
-            callbackURL: localePath("/dashboard"),
-            errorCallbackURL: localePath("/auth/sign-in"),
+            callbackURL,
+            errorCallbackURL,
         });

Also applies to: 116-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/sign-in.vue` around lines 95 - 103, The SSO flow in
handleSelfHostedSso (and the related SSO handler around lines 116-131) always
redirects to localePath("/dashboard") and thus drops route.query.invitation;
update the authClient.signIn.oauth2 call(s) to preserve invitation by building
callbackURL and errorCallbackURL that include the invitation query when present
(e.g., if route.query.invitation exists append ?invitation=... or merge into
existing query), mirroring the behavior in handleSignIn so successful and failed
SSO returns keep the invite context.
🧹 Nitpick comments (1)
tests/unit/sso-integration-readiness.test.ts (1)

268-278: Avoid mirroring resolveBetterAuthUrl logic inside tests.

Re-implementing production logic in-test risks lockstep bugs and false positives. Prefer importing the real function (or extracting/exporting it) and testing that directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/sso-integration-readiness.test.ts` around lines 268 - 278, The
test defines a duplicate implementation of resolveBetterAuthUrl which risks
diverging from production; instead import the real resolveBetterAuthUrl from its
source (or extract and export it from the production module into a shared util)
and use that in tests; update tests/unit/sso-integration-readiness.test.ts to
remove the inline resolveBetterAuthUrl function and replace calls with the
imported resolveBetterAuthUrl so the test exercises actual production logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/auth/sign-in.vue`:
- Around line 55-60: Wrap the call to authClient.signIn.email({ email:
email.value, password: password.value }) in a try/catch/finally block so
isLoading.value is always reset; in the try run the sign-in, in catch set a
user-facing error state (e.g., assign to your existing error variable or create
one) with the caught error message, and in finally set isLoading.value = false
to guarantee recovery even on network/runtime exceptions.

In `@tests/unit/sso-integration-readiness.test.ts`:
- Around line 199-228: Replace the ad-hoc rolePermissions map and the local
hasPermission helper in the test with assertions that import and use the
authoritative permission definitions used by the application (e.g., the shared
permission matrix or policy object) so tests reflect real authorization;
specifically, remove the local rolePermissions and hasPermission and import the
shared permission source (the module that exports the app's role-to-permissions
mapping or policy) and call its lookup/check method (or use the same utility the
app uses to evaluate resource/action permissions) to assert that 'owner' and
'admin' have organization:update and 'member' and unknown roles do not.
- Around line 236-261: Replace the tautological literals in
tests/unit/sso-integration-readiness.test.ts with assertions against the real
OIDC config produced by your auth utilities: import the OIDC config
builder/export from server/utils/auth.ts (e.g., the function that returns
global/per-organization oidc config) and call it to get the runtime object, then
assert its pkce, requireIssuerValidation and scopes include 'openid' (and verify
the object shape passed to registerSSOProvider for per-org config) instead of
testing hardcoded local variables.

---

Duplicate comments:
In `@app/pages/auth/sign-in.vue`:
- Around line 95-103: The SSO flow in handleSelfHostedSso (and the related SSO
handler around lines 116-131) always redirects to localePath("/dashboard") and
thus drops route.query.invitation; update the authClient.signIn.oauth2 call(s)
to preserve invitation by building callbackURL and errorCallbackURL that include
the invitation query when present (e.g., if route.query.invitation exists append
?invitation=... or merge into existing query), mirroring the behavior in
handleSignIn so successful and failed SSO returns keep the invite context.

In `@app/pages/dashboard/settings/sso.vue`:
- Line 425: The template is referencing window directly which the Vue compiler
treats as an undefined template property; add a computed property named
appOrigin in the <script setup> that returns window.location.origin when running
client-side (use import.meta.client) and returns the fallback
'https://app.reqcore.com' for SSR, then replace the inline window expression in
the template with the appOrigin computed value (use
`${appOrigin}/api/auth/sso/callback/{provider-id}`) so the template no longer
references window directly.

In `@server/utils/env.ts`:
- Around line 114-123: The env schema uses
emptyToUndefined.pipe(z.string().min(1)).optional(), which still fails when
emptyToUndefined turns "" into undefined because the inner schema rejects
undefined; update the three entries (OIDC_CLIENT_ID, OIDC_CLIENT_SECRET,
OIDC_DISCOVERY_URL) to move .optional() inside the pipe so the inner schema
accepts undefined (e.g., emptyToUndefined.pipe(z.string().min(1).optional()) for
the first two and emptyToUndefined.pipe(z.string().url().refine(...).optional())
for OIDC_DISCOVERY_URL), ensuring empty strings are treated as unset while
preserving the existing validations.

---

Nitpick comments:
In `@tests/unit/sso-integration-readiness.test.ts`:
- Around line 268-278: The test defines a duplicate implementation of
resolveBetterAuthUrl which risks diverging from production; instead import the
real resolveBetterAuthUrl from its source (or extract and export it from the
production module into a shared util) and use that in tests; update
tests/unit/sso-integration-readiness.test.ts to remove the inline
resolveBetterAuthUrl function and replace calls with the imported
resolveBetterAuthUrl so the test exercises actual production logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8aee1da-8af1-42ee-91a6-0f50c10233ca

📥 Commits

Reviewing files that changed from the base of the PR and between 62fdf39 and d9c2100.

📒 Files selected for processing (14)
  • app/components/SettingsSidebar.vue
  • app/pages/auth/sign-in.vue
  • app/pages/auth/sign-up.vue
  • app/pages/dashboard/settings/sso.vue
  • server/api/sso/providers.get.ts
  • server/api/sso/providers.post.ts
  • server/api/sso/providers/[id].delete.ts
  • server/utils/env.ts
  • tests/unit/sso-auth-config.test.ts
  • tests/unit/sso-env-extended.test.ts
  • tests/unit/sso-integration-readiness.test.ts
  • tests/unit/sso-providers-api.test.ts
  • tests/unit/sso-security.test.ts
  • tests/unit/sso-trusted-origins.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/components/SettingsSidebar.vue
  • server/api/sso/providers.post.ts
  • server/api/sso/providers/[id].delete.ts
  • app/pages/auth/sign-up.vue

Comment thread app/pages/auth/sign-in.vue Outdated
Comment on lines +199 to +228
// Simulate Better Auth's default org permission structure
// owner and admin have organization:update, member does not
const rolePermissions: Record<string, { organization: string[] }> = {
owner: { organization: ['create', 'update', 'delete'] },
admin: { organization: ['create', 'update'] },
member: { organization: [] },
}

function hasPermission(role: string, resource: string, action: string): boolean {
const perms = rolePermissions[role]
if (!perms) return false
const actions = (perms as Record<string, string[]>)[resource]
return actions?.includes(action) ?? false
}

it('owner can manage SSO providers', () => {
expect(hasPermission('owner', 'organization', 'update')).toBe(true)
})

it('admin can manage SSO providers', () => {
expect(hasPermission('admin', 'organization', 'update')).toBe(true)
})

it('member CANNOT manage SSO providers', () => {
expect(hasPermission('member', 'organization', 'update')).toBe(false)
})

it('unknown role CANNOT manage SSO providers', () => {
expect(hasPermission('unknown-role', 'organization', 'update')).toBe(false)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Permission-boundary tests are disconnected from real role definitions.

This local rolePermissions map can diverge from actual roles/policies, so these tests may pass while authorization is broken. Import and assert against the shared permission source used by auth configuration instead of a handcrafted matrix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/sso-integration-readiness.test.ts` around lines 199 - 228, Replace
the ad-hoc rolePermissions map and the local hasPermission helper in the test
with assertions that import and use the authoritative permission definitions
used by the application (e.g., the shared permission matrix or policy object) so
tests reflect real authorization; specifically, remove the local rolePermissions
and hasPermission and import the shared permission source (the module that
exports the app's role-to-permissions mapping or policy) and call its
lookup/check method (or use the same utility the app uses to evaluate
resource/action permissions) to assert that 'owner' and 'admin' have
organization:update and 'member' and unknown roles do not.

Comment on lines +236 to +261
it('global OIDC config has PKCE enabled', () => {
const globalOidcConfig = {
scopes: ['openid', 'email', 'profile'],
pkce: true,
requireIssuerValidation: true,
}
expect(globalOidcConfig.pkce).toBe(true)
expect(globalOidcConfig.requireIssuerValidation).toBe(true)
})

it('enterprise SSO config has PKCE in oidcConfig', () => {
// This is what gets passed to Better Auth's registerSSOProvider
const perOrgConfig = {
clientId: 'test-client',
clientSecret: 'test-secret',
scopes: ['openid', 'email', 'profile'],
pkce: true,
}
expect(perOrgConfig.pkce).toBe(true)
expect(perOrgConfig.scopes).toContain('openid')
})

it('scopes always include openid (required for OIDC)', () => {
const scopes = ['openid', 'email', 'profile']
expect(scopes.includes('openid')).toBe(true)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

PKCE/issuer tests are tautological and don’t validate runtime auth config.

These assertions check hardcoded literals created inside the test, not the actual OIDC config in server/utils/auth.ts. The suite currently can’t catch regressions in real plugin options.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/sso-integration-readiness.test.ts` around lines 236 - 261, Replace
the tautological literals in tests/unit/sso-integration-readiness.test.ts with
assertions against the real OIDC config produced by your auth utilities: import
the OIDC config builder/export from server/utils/auth.ts (e.g., the function
that returns global/per-organization oidc config) and call it to get the runtime
object, then assert its pkce, requireIssuerValidation and scopes include
'openid' (and verify the object shape passed to registerSSOProvider for per-org
config) instead of testing hardcoded local variables.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-136 April 10, 2026 10:37 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-136 April 10, 2026 10:52 Destroyed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/pages/auth/sign-up.vue (1)

53-61: ⚠️ Potential issue | 🟠 Major

Handle thrown sign-up failures before checking result.error.

authClient.signUp.email(...) can still throw on transport/runtime failures, and this path currently leaves isLoading stuck at true with no visible error. Please wrap the await the same way handleSignIn() does on the sign-in page.

🛠️ Suggested hardening
     isLoading.value = true;

     track("signup_submitted");

-    const result = await authClient.signUp.email({
-        email: email.value,
-        password: password.value,
-        name: name.value,
-    });
+    let result: Awaited<ReturnType<typeof authClient.signUp.email>>;
+    try {
+        result = await authClient.signUp.email({
+            email: email.value,
+            password: password.value,
+            name: name.value,
+        });
+    } catch (e: unknown) {
+        error.value =
+            e instanceof Error ? e.message : "Sign-up failed. Please try again.";
+        track("signup_failed", { error_type: "exception" });
+        isLoading.value = false;
+        return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/sign-up.vue` around lines 53 - 61, The sign-up flow can throw
from authClient.signUp.email(...) and currently leaves isLoading.value true with
no error handling; wrap the await call in a try/catch similar to handleSignIn(),
set isLoading.value = false in both success and error paths (use finally or
explicit sets), handle thrown errors by assigning a user-visible error (e.g.,
signUpError or the same error state used by handleSignIn) and log/track the
failure, and still check result.error after the awaited call; update the block
around authClient.signUp.email to mirror the error/cleanup pattern used in
handleSignIn().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/auth/sign-in.vue`:
- Around line 15-19: Replace the two separate reactive flags isLoading and
ssoRedirecting with a single shared busy flag (e.g., busy) and update all
references (initial declarations and usages inside the email sign-in handler,
SSO redirect handler, and button disabled bindings) so that starting either auth
flow sets busy = true and both buttons check busy to disable; ensure the flag is
cleared on both success and any error paths in the functions that currently set
isLoading or ssoRedirecting (look for handlers that reference isLoading and
ssoRedirecting and the template bindings that disable buttons).

In `@server/utils/auth.ts`:
- Around line 51-63: The current code in auth.ts loads all ssoProvider rows
(db.select from schema.ssoProvider) and unions every issuer origin into the
global trustedOrigins (idpOrigins/staticOrigins), which both broadens the CSRF
allowlist across tenants and causes a full-table read; instead, change the
lookup to resolve only the issuer relevant to the current SSO flow: accept the
incoming issuer/tenant identifier from the request context, query
schema.ssoProvider with a WHERE filter (e.g., where schema.ssoProvider.issuer =
:issuer OR where tenant_id = :tenantId) using a parameterized db.select, parse
that single provider's issuer URL into an origin, and merge that origin with
staticOrigins (preserving the Set de-dup step) so you no longer read or trust
all rows globally.

---

Duplicate comments:
In `@app/pages/auth/sign-up.vue`:
- Around line 53-61: The sign-up flow can throw from
authClient.signUp.email(...) and currently leaves isLoading.value true with no
error handling; wrap the await call in a try/catch similar to handleSignIn(),
set isLoading.value = false in both success and error paths (use finally or
explicit sets), handle thrown errors by assigning a user-visible error (e.g.,
signUpError or the same error state used by handleSignIn) and log/track the
failure, and still check result.error after the awaited call; update the block
around authClient.signUp.email to mirror the error/cleanup pattern used in
handleSignIn().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46003dc5-b8e5-4936-8ca8-0e2a81d5c8af

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5aa68 and 76c54b4.

📒 Files selected for processing (4)
  • app/pages/auth/sign-in.vue
  • app/pages/auth/sign-up.vue
  • server/api/sso/providers/[id].delete.ts
  • server/utils/auth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/api/sso/providers/[id].delete.ts

Comment on lines +15 to +19
const email = ref("");
const password = ref("");
const error = ref("");
const isLoading = ref(false);
const ssoRedirecting = ref(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use one busy state for both auth paths.

isLoading and ssoRedirecting currently disable different buttons, so users can kick off email sign-in and enterprise SSO at the same time. Disable both actions off a shared busy flag to avoid racing two auth flows.

🔧 Suggested simplification
 const error = ref("");
 const isLoading = ref(false);
 const ssoRedirecting = ref(false);
+const authBusy = computed(() => isLoading.value || ssoRedirecting.value);
-            :disabled="isLoading"
+            :disabled="authBusy"
-                :disabled="ssoRedirecting"
+                :disabled="authBusy"

Also applies to: 240-246, 264-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/auth/sign-in.vue` around lines 15 - 19, Replace the two separate
reactive flags isLoading and ssoRedirecting with a single shared busy flag
(e.g., busy) and update all references (initial declarations and usages inside
the email sign-in handler, SSO redirect handler, and button disabled bindings)
so that starting either auth flow sets busy = true and both buttons check busy
to disable; ensure the flag is cleared on both success and any error paths in
the functions that currently set isLoading or ssoRedirecting (look for handlers
that reference isLoading and ssoRedirecting and the template bindings that
disable buttons).

Comment thread server/utils/auth.ts
Comment on lines +51 to +63
// Dynamically load registered SSO provider issuers
try {
const providers = await db
.select({ issuer: schema.ssoProvider.issuer })
.from(schema.ssoProvider);

const idpOrigins = providers
.map((p) => {
try { return new URL(p.issuer).origin; } catch { return null; }
})
.filter((o): o is string => o !== null);

return Array.from(new Set([...staticOrigins, ...idpOrigins]));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't trust every registered issuer globally.

This query unions all sso_provider.issuer origins into trustedOrigins for any SSO request. That lets one tenant's provider registration expand the CSRF allowlist for every other tenant, and it also turns each SSO auth hit into a full-table read. Resolve the issuer for the current SSO flow only instead of loading every row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/auth.ts` around lines 51 - 63, The current code in auth.ts loads
all ssoProvider rows (db.select from schema.ssoProvider) and unions every issuer
origin into the global trustedOrigins (idpOrigins/staticOrigins), which both
broadens the CSRF allowlist across tenants and causes a full-table read;
instead, change the lookup to resolve only the issuer relevant to the current
SSO flow: accept the incoming issuer/tenant identifier from the request context,
query schema.ssoProvider with a WHERE filter (e.g., where
schema.ssoProvider.issuer = :issuer OR where tenant_id = :tenantId) using a
parameterized db.select, parse that single provider's issuer URL into an origin,
and merge that origin with staticOrigins (preserving the Set de-dup step) so you
no longer read or trust all rows globally.

@JoachimLK JoachimLK merged commit 698729d into main Apr 10, 2026
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 24, 2026
9 tasks
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