fix(people): create invited employees with verified emails so SSO can link#3459
Conversation
… link Employees added via the invite flow (people-invite.service) and the add-people flow (lib/db/employee) were created with emailVerified: false. better-auth refuses to link a trusted OAuth provider (Google/Microsoft) sign-in to an existing unverified local user and aborts the callback with account_not_linked, so those employees could not sign in to the portal with SSO at all — this was the dominant OAuth error in production logs, and it hit Google and Microsoft equally. Create the user rows with emailVerified: true instead, and backfill existing org members with a data migration so already-invited employees are unblocked too. This is safe because password auth is disabled: every sign-in method (email OTP, magic link, trusted OAuth) proves control of the mailbox before a session is issued, so the flag grants nothing to anyone who cannot already receive mail at the address. The employee-sync paths (Google Workspace etc.) already create users with emailVerified: true; this aligns the manual paths with them. Intentionally NOT changed: better-auth's accountLinking config (requireLocalEmailVerified is deprecated and slated to become unconditional, so relying on it would re-break on a future upgrade).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
3 issues found across 5 files
Confidence score: 2/5
- In
apps/app/src/lib/db/employee.ts, the new verified-user creation path appears reachable by any active org member without enforcingmember:create, so non-admin users could create employee records the code assumes are admin-provided; this is a likely authorization regression—add an explicit permission gate on this route before merging. - In
apps/app/src/lib/db/employee.ts,emailVerifiedis being repurposed from an email-verification invariant to an admin-input flag, which can let downstream checks (for example in org access logic) treat users as verified when provider email trust is not actually established; that can weaken access assumptions—separate admin intent from true email verification or tighten the downstream predicate before merging. - In
apps/api/src/people/people-invite.service.ts, the existing-user invite branch does not apply the same verification update as the new-user insert branch, so already-unverified users may still fail Google/Microsoft account linking after invite; this creates a user-facing onboarding dead end—apply the same verification handling in the existing-user path and add a regression test.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/app/src/lib/db/employee.ts">
<violation number="1" location="apps/app/src/lib/db/employee.ts:175">
P1: This changes `emailVerified` from an email-verification invariant into an admin-input flag, while trusted providers can link without a verified provider email; downstream checks such as `OrganizationAccessService.isEmailVerified` then treat that address as verified. Keeping the row unverified until a mailbox challenge, or tightening trusted-provider/linking and auto-approval rules, would avoid granting verification-dependent behavior from an unverified address.</violation>
<violation number="2" location="apps/app/src/lib/db/employee.ts:183">
P1: Any active organization member can reach this new verified-user creation path without the `member:create` permission, so the comment's assumption that the address is admin-provided is not enforced. Routing employee creation through the API's `member:create` RBAC guard would prevent unauthorized members from provisioning SSO-linkable organization accounts.</violation>
</file>
<file name="apps/api/src/people/people-invite.service.ts">
<violation number="1" location="apps/api/src/people/people-invite.service.ts:179">
P2: Inviting an already-existing unverified user still leaves them unable to link Google/Microsoft SSO. The new flag is applied only to newly inserted users; the existing-user branch should also verify the user when adding them as an employee, or otherwise ensure this membership transition is covered.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Review follow-ups: - Re-inviting a user whose row predates created-verified behavior (and who was not a member when the backfill ran, e.g. after their previous org was deleted) left them unverified and still unable to link Google/Microsoft sign-ins. Both invite paths (employee add + role invitation) now upgrade an existing unverified row to verified, with the same justification as creation: the address is admin-provided and every sign-in method proves mailbox ownership before a session is issued. No-op when already verified. - createEmployeeAction (and its completeEmployeeCreation helper) had no callers anywhere in the app: the live add-people flow goes through the API, where POST /v1/people/invite, POST /v1/people and /bulk all enforce member:create. Rather than leave an unreferenced server action that creates users and members with no permission check of its own, delete it and prune lib/db/employee.ts to the one export that is actually used (createTrainingVideoEntries).
|
Addressed the review findings in 3305c3b — disposition of each: 1. Existing-user invite branch left legacy rows unverified (P2 — valid, fixed). Both invite paths (employee add + role invitation) now upgrade an existing unverified row via a shared 2. 3. Verification: |
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
## [3.104.2](v3.104.1...v3.104.2) (2026-07-20) ### Bug Fixes * **auth:** redirect microsoft sso callback errors to portal instead of api docs ([7920bae](7920bae)) * **integrations:** report code scanning off vs missing permission correctly ([220d60f](220d60f)) * **people:** create invited employees with verified emails so SSO can link ([#3459](#3459)) ([cd31daf](cd31daf))
|
🎉 This PR is included in version 3.104.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Problem
Invited employees frequently could not sign in to the portal with Google or Microsoft SSO. The OAuth callback aborted with
account_not_linked— the dominant Better Auth error in production logs over the last 3 weeks (~70 occurrences, both providers equally).Root cause
better-auth refuses to link a trusted OAuth provider to an existing local user whose
emailVerifiedisfalse(therequireLocalEmailVerifiedgate, defaulttrue). Both manual employee-creation paths created users unverified:apps/api/src/people/people-invite.service.ts(invite flow)apps/app/src/lib/db/employee.ts(add-people flow)So: admin invites employee → user row created with
emailVerified: false→ employee clicks "Continue with Google/Microsoft" → better-auth finds the existing user, refuses to link → sign-in fails. (Employees who happened to use email OTP first were unaffected, which is why the failure looked intermittent.)Related: #3454 (CS-760) fixed where the failing user lands after the error; this PR fixes the error itself.
Fix
emailVerified: truein both paths (2-line behavioral change + comments).Why this is safe
emailVerified: true. This aligns the manual paths with them.emailVerifiedread (organization-access.service.tstrycomp.ai auto-approve) uses the session user's email behindHybridAuthGuard+organization:update, so it cannot be influenced by unauthenticated rows.Intentionally NOT changed
auth.server.ts/accountLinkingconfig:requireLocalEmailVerified: falsewould also fix this, but the option is@deprecatedin better-auth ("removed on next minor; the gate becomes unconditional") — relying on it would silently re-break on a future upgrade.state_mismatch(~10 occurrences/3 weeks) is a separate, minor issue (stale tab / double-click), not addressed here.Verification
cd apps/api && npx jest src/people— 102/102 pass (incl. new test asserting invited users are created verified).cd apps/app && npx vitest run src/lib/db/employee.test.ts— 2/2 pass (new-user path creates verified; existing-user path never touches the user row).Summary by cubic
Fixes SSO failures for invited employees by creating users with verified emails and upgrading legacy users on re-invite so
better-authcan link Google/Microsoft accounts, removingaccount_not_linkederrors. Also removes unused app-side employee creation code. Addresses Linear CS-760.Bug Fixes
emailVerified: truein the invite flow.Migration
emailVerified: falsetotrue; safe since password auth is disabled and all sign-in methods prove mailbox control.Written for commit 3305c3b. Summary will update on new commits.