Skip to content

fix(auth): harden ClerkUserSyncService against account takeover, username collisions, and invalid inputs (#115) - #171

Merged
VarshithReddy2006 merged 2 commits into
VarshithReddy2006:mainfrom
Subodh-17:fix/115-clerk-user-sync-hardening
Nov 11, 2026
Merged

VarshithReddy2006 merged 2 commits into
VarshithReddy2006:mainfrom
Subodh-17:fix/115-clerk-user-sync-hardening

Conversation

@Subodh-17

Copy link
Copy Markdown
Contributor

Summary of Changes

This pull request resolves Issue #115 by hardening ClerkUserSyncService across input validation, username generation, concurrency resilience, and cross-identity account linking.

Key architectural improvements include:

  1. Account-Takeover Prevention: Fixed a critical cross-identity account linking vulnerability. Email addresses from Clerk payloads are now strictly validated against verification.status === 'verified'. Missing, null, unverified, or non-verified email metadata strictly fails closed with 409 UNVERIFIED_EMAIL_CONFLICT. Furthermore, existing accounts linked to an established clerkId cannot be re-linked to a different Clerk identity (409 CLERK_IDENTITY_CONFLICT).
  2. Schema-Aligned Username Sanitization: Aligned username validation (isSchemaValidUsername) with Mongoose User schema constraints ({ minlength: 3, maxlength: 30, trim: true }), preserving valid usernames containing whitespace without triggering silent renames.
  3. Collision & Concurrency Recovery: Replaced naive retry loops with an exponential/random-suffix derivation strategy bounded at 12 attempts for both new user registration and existing user backfilling. Handled MongoDB E11000 duplicate key errors gracefully across clerkId, email, and username unique indexes.
  4. Partial Provisioning Self-Healing: Wrapped supporting record provisioning (UserProfile, UserSettings) with existence guards and userId_1 duplicate key absorption. Interrupted provisioning automatically self-heals on subsequent user logins.
  5. Sanitized Error Exposure: Replaced raw MongoServerError rethrows with structured, sanitized AppError instances, preventing internal database connection strings or query details from leaking in API responses.

Fixes #115


Acceptance Criteria & Issue #115 Matrix

Criterion Implementation Details Status
1. Missing / Malformed Email Validates email syntax. Cleanly rejects missing/empty emails with 400 BAD_REQUEST (EMAIL_REQUIRED). Addressed / Clarification Requested (See note below)
2. Unsafe Username Derivation Derives safe base username from email handle or first/last names. Enforces schema length constraints [3, 30]. Implemented & Verified
3. Username Length Bounds Pads strings shorter than 3 characters (user_<hash>); slices handles longer than 30 characters before suffix derivation. Implemented & Verified
4. Uniqueness / Collision Resolves E11000 duplicate key conflicts with bounded random/numeric suffix retries (max 12). Implemented & Verified
5. Partial Provisioning Ensures UserProfile and UserSettings exist; handles race conditions gracefully. Implemented & Verified
6. Account-Linking Security Prohibits unverified email linking and cross-identity hijacking. Implemented & Hardened

Maintainer Clarification Question (Criterion #1: Missing Email)

Important

Maintainer Clarification Requested regarding Missing Email Handling:

Acceptance criterion #1 in Issue #115 states:
"If Clerk payload has no email or empty email_addresses array, sync should succeed without email instead of crashing."

However, the User schema in backend/src/models/user.model.ts explicitly enforces:

email: { type: String, required: true, unique: true, lowercase: true, trim: true }

Creating a User record without an email immediately fails Mongoose document validation (ValidationError: Path 'email' is required.), and throughout DevTrack, core controllers and auth middleware depend on user.email.

Current PR Behavior: The sync service cleanly rejects payloads without a valid email with a structured AppError(400, 'Email address is required for user synchronization', 'EMAIL_REQUIRED') rather than crashing with unhandled runtime errors (TypeError: Cannot read properties of undefined).

Question for Maintainers: Does DevTrack plan to support optional/email-less accounts in the future (which would require migrating the Mongoose schema and unique index on email), or is the strict 400 EMAIL_REQUIRED rejection the desired production contract?


Verification & Test Results

  • New Regression Test Suite: backend/src/__tests__/services/auth/ClerkUserSyncService.test.ts

    • 22 tests executed, 22 passed, 0 failed in 5.09s.
    • Tests cover:
      1. Successful sync of brand new user with profile and settings provisioning.
      2. Idempotent re-sync of existing user without duplicate provisioning.
      3. Missing email rejection (400 EMAIL_REQUIRED).
      4. Malformed email rejection (400 INVALID_EMAIL_FORMAT).
      5. Safe base username derivation from email local-part.
      6. Preservation of existing schema-valid usernames containing spaces.
      7. Sanitization of usernames with prohibited special characters.
      8. Padding of short usernames (<3 characters).
      9. Truncation and suffixing of long usernames (>30 characters).
      10. Recovery from initial username collision (E11000).
      11. Bounded retry exhaustion failure on repeated collisions.
      12. Secure account linking for pre-existing verified email users without clerkId.
      13. Account hijacking prevention for unverified Clerk emails (409 UNVERIFIED_EMAIL_CONFLICT).
      14. Account hijacking prevention for null/missing verification status (409 UNVERIFIED_EMAIL_CONFLICT).
      15. Cross-Clerk-identity takeover prevention on established accounts (409 CLERK_IDENTITY_CONFLICT).
      16. Self-healing of missing UserProfile on subsequent sync.
      17. Self-healing of missing UserSettings on subsequent sync.
      18. Race condition handling for concurrent UserProfile creation.
      19. Race condition handling for concurrent UserSettings creation.
      20. Fallback username generation when first/last names are missing.
      21. Unlinked user account linking preserving existing username when valid.
      22. Non-leakage of database credentials or query internals in thrown errors.
  • Existing Test Suites & Build:

    • npm run build: Exit code 0 (tsc -p tsconfig.json && node fix-esm-imports.mjs).
    • src/__tests__/middleware/: 9 passed, 0 failed.
    • tests/integration-database.test.ts: 16 passed, 0 failed.
    • src/__tests__/integration/: 8 passed, 0 failed.
  • Baseline Disclosure:

    • src/__tests__/db/modelIndexes.test.ts reports a failure (missing: ['notification.model.ts']). Independent verification against a clean checkout of base commit 5a57144 confirmed this is an existing baseline failure in the repository, unrelated to this PR.

Reviewer Checklist

  • Email verification status is strictly required for account linking (status === 'verified').
  • Existing accounts with a different clerkId cannot be overwritten.
  • Valid usernames (including those with internal spaces) are preserved.
  • Username collision derives random/numeric suffixes with a bounded retry limit.
  • Supporting profiles (UserSettings, UserProfile) self-heal without duplicate-key errors.
  • All 22 targeted unit tests pass without warnings.
  • TypeScript build succeeds with exit code 0.

@VarshithReddy2006 VarshithReddy2006 added bug Something isn't working nsoc26 level2 5 points - UI, frontend components, layouts, styling, and page-level changes. security Security-related improvements labels Nov 11, 2026

@VarshithReddy2006 VarshithReddy2006 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM! Thanks for the thorough security hardening and regression test suite @Subodh-17.

To answer your clarification question: yes, keeping strict 400 EMAIL_REQUIRED / INVALID_EMAIL is the intended contract given User.email is required: true, unique: true in the schema and auth/user flows depend on a real email address.

I pushed a small fix (ae7f245) switching ensureSupportingRecords from Promise.all to Promise.allSettled before rethrowing so concurrent UserSettings/UserProfile creation settles cleanly on partial failure (which fixed the flaky/failing assertion in test #12). Verified ClerkUserSyncService.test.ts (22/22), test:safety (16/16), and backend TypeScript build all pass.

@VarshithReddy2006
VarshithReddy2006 merged commit 438c472 into VarshithReddy2006:main Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working level2 5 points - UI, frontend components, layouts, styling, and page-level changes. nsoc26 security Security-related improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(auth): handle missing email, length constraints, and username collisions in ClerkUserSyncService

2 participants