Skip to content

Conversation

SNtGog
Copy link

@SNtGog SNtGog commented Sep 5, 2025

Pull Request

Issue

Closes: #9855


Approach

Summary

A single, targeted fix in the auth path:

  • Before calling Auth.findUsersWithAuthData, build a filtered object that excludes providers with null or undefined values.
  • Apply unlink markers (authData[provider] = null) as part of the update without invoking the provider adapter.
  • No other behavior or API changes.

Behavior & Compatibility

  • Unlinking remains explicit via authData[provider] = null.
  • Non-null providers follow the existing validation and conflict checks.
  • No changes to session token handling, email verification flow, or public APIs.
  • No custom diff/equality logic introduced.

Security & Performance

  • Prevents accidental adapter calls (e.g., OAuth code→token exchange) during unlink.
  • Keeps the change surface minimal in a sensitive path; reduces unnecessary external validations.

Tests

  • (Added/updated) scenarios:

    • Unlink-only update: { provider: null } unlinks without adapter validation.

Release Notes (proposed)

fix(auth): Ignore null/undefined providers from authData during user lookup; unlink (provider = null) occurs without adapter validation. No other behavior changes.

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)
  • Add security checks

Summary by CodeRabbit

  • Bug Fixes

    • Unlinking third‑party auth providers now works reliably when setting a provider to null/undefined in auth data. Provider entries are correctly removed from user accounts, preventing stale linkage. Applies broadly (e.g., Google Play Games) with no API changes.
  • Tests

    • Added unit tests validating the unlink flow for Google Play Games, including successful login, unlinking via auth data update, and verification that provider data is cleared.

Refactor authData processing for improved clarity, efficiency, and correctness. Introduce `diffAuthData` and `subsetEqual` to handle partial updates, unlinking, and change detection intelligently. Update `RestWrite` logic to utilize refined authData delta behavior, preventing redundant operations. Add comprehensive test coverage for edge cases and optimizations. Refactor existing password policy validations for readability.
Copy link

I will reformat the title to use the proper commit message syntax.

@parse-github-assistant parse-github-assistant bot changed the title feat(auth): delta-based authData updates; skip redundant validation; explicit unlink semantics feat(auth): Delta-based authData updates; skip redundant validation; explicit unlink semantics Sep 5, 2025
Copy link

parse-github-assistant bot commented Sep 5, 2025

🚀 Thanks for opening this pull request!

Copy link

coderabbitai bot commented Sep 5, 2025

📝 Walkthrough

Walkthrough

The change updates RestWrite.handleAuthData to ignore null/undefined providers by filtering them out before calling Auth.findUsersWithAuthData. A new unit test verifies that setting authData.gpgames to null unlinks the provider without triggering validation or external calls.

Changes

Cohort / File(s) Summary of Edits
Auth unlink handling
src/RestWrite.js
Filters null/undefined providers from incoming authData into a new object and passes it to Auth.findUsersWithAuthData; leaves remaining validation and ACL logic unchanged.
Unit tests for unlink
spec/RestWrite.handleAuthData.spec.js
Adds tests ensuring authData.gpgames set to null unlinks without OAuth code/token validation, using mocked external endpoints and server config for gpgames.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant RW as RestWrite.handleAuthData
  participant Auth as Auth API
  participant Prov as Provider Adapter

  Client->>RW: Save User with authData (may include provider=null)
  alt Has authData
    RW->>RW: Filter out null/undefined providers (unlink entries)
    RW->>Auth: findUsersWithAuthData(filteredAuthData, useMaster=true)
    Auth-->>RW: Matching users (ACL-filtered)
    alt Providers remain after filtering
      RW->>Prov: Validate remaining provider authData
      Prov-->>RW: Validation result
    else No providers remain (only unlinks)
      note over RW: Skip provider validation
    end
    RW-->>Client: Proceed with save (unlinked providers removed)
  else No authData
    RW-->>Client: Proceed with save
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Pre-merge checks (4 passed, 1 warning)

❌ Failed Checks (1 warning)
Check Name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning Beyond the unlink fix, the PR introduces a generic diffAuthData/subsetEqual refactoring for multi-provider updates and refactors password policy validation code, which are not required by issue #9855’s scope and introduce unrelated changes. Split out the generic authData diff and password policy refactor into a separate PR, and keep this PR focused solely on the null-authData unlink handling and validation skip.
✅ Passed Checks (4 passed)
Check Name Status Explanation
Title Check ✅ Passed The title concisely describes the primary change: treating authData[provider] set to null as an explicit unlink operation and skipping provider validation during unlink, which matches the core modifications in the PR.
Linked Issues Check ✅ Passed The PR implements the expected explicit unlink semantics by filtering out null authData entries before validation, skipping adapter code/token exchanges for unlinked providers, and includes tests verifying the unlink behavior, thus satisfying the objectives of issue #9855.
Description Check ✅ Passed The description includes all required template sections (Pull Request with security/license links, Issue referencing the closed issue, Approach detailing the change, and Tasks checklist) and follows the prescribed structure without missing any mandatory information.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
		  - name: "Undocumented Breaking Changes"
			  mode: "warning"
			  instructions: |
				  Flag potential breaking changes that are not documented:
				  1. Identify changes to public APIs/exports, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints (including removed/renamed items and changes to types, required params, return values, defaults, or behavior).
				  2. Ignore purely internal/private changes (e.g., code not exported from package entry points or marked internal).
				  3. Verify documentation exists: a "Breaking Change" section in the PR description and updates to CHANGELOG.md.

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.

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Sep 5, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

Copy link

@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: 0

🧹 Nitpick comments (6)
src/RestWrite.js (1)

1045-1054: Clean refactor of email verification conditions.

The extraction of verification logic into arrow functions improves readability without changing behavior.

spec/Users.authdata.spec.js (5)

84-92: Prefer creating the initial user via logInWith for clarity and stability.

Creating a user by new Parse.User() + set('authData') + save() can be environment-sensitive. Using logInWith ensures session issuance and consistent auth semantics.

-      const user = new Parse.User();
-      user.set('authData', authData);
-      await user.save();
+      const user = await Parse.User.logInWith('gpgames', { authData: authData.gpgames });
+      // Link someAdapter2 immediately to mirror the original initial state
+      user.set('authData', { someAdapter2: authData.someAdapter2 });
+      await user.save(null, { sessionToken: user.getSessionToken() });

125-135: Assert no provider validation/API calls during unlink.

Strengthen the unlink test by asserting that no extra fetch calls occur when gpgames: null is sent.

-      user.set('authData', {
+      const beforeCalls = global.fetch?.calls?.count ? global.fetch.calls.count() : 0;
+      user.set('authData', {
         ...currentAuthData,
         gpgames: null,
       });
       await user.save(null, { sessionToken });
@@
-      expect(finalAuthData.gpgames).toBeUndefined();
+      expect(finalAuthData.gpgames).toBeUndefined();
+      if (global.fetch?.calls?.count) {
+        expect(global.fetch.calls.count()).toBe(beforeCalls);
+      }

149-151: Remove the artificial delay to keep tests fast and deterministic.

No timestamp-sensitive fields are compared here; the sleep only slows the suite.

-      // Small delay to ensure timestamp differences don't affect comparison
-      await new Promise(resolve => setTimeout(resolve, 100));

210-214: Drop nested reconfiguration; rely on the outer beforeEach.

Reconfiguring the server again per test here adds overhead without benefit.

-    beforeEach(async () => {
-      await setupAuthConfig();
-    });

224-229: Use master key when introspecting authData to avoid protected-field filtering.

Some environments don’t expose authData to clients even with a valid session. Using useMasterKey: true prevents flakiness.

-      const freshUser = await new Parse.Query(Parse.User).get(user.id, { sessionToken });
+      const freshUser = await new Parse.Query(Parse.User).get(user.id, { useMasterKey: true });
@@
-      const finalUser = await new Parse.Query(Parse.User).get(user.id, { sessionToken });
+      const finalUser = await new Parse.Query(Parse.User).get(user.id, { useMasterKey: true });

Also applies to: 253-259

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 82fdb0d and 7669bd9.

📒 Files selected for processing (4)
  • spec/AuthenticationAdaptersV2.spec.js (1 hunks)
  • spec/Users.authdata.spec.js (1 hunks)
  • src/Auth.js (3 hunks)
  • src/RestWrite.js (9 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-09T09:59:06.289Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1340-1375
Timestamp: 2025-05-09T09:59:06.289Z
Learning: Tests in the parse-server repository should use promise-based approaches rather than callback patterns with `done()`. Use a pattern where a Promise is created that resolves when the event occurs, then await that promise.

Applied to files:

  • src/RestWrite.js
📚 Learning: 2025-05-09T09:59:06.289Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1340-1375
Timestamp: 2025-05-09T09:59:06.289Z
Learning: New tests in the parse-server repository should use async/await with promise-based patterns rather than callback patterns with `done()`. The preferred pattern is to create a Promise that resolves when an expected event occurs, then await that Promise.

Applied to files:

  • src/RestWrite.js
🧬 Code graph analysis (4)
spec/AuthenticationAdaptersV2.spec.js (1)
spec/AuthenticationAdapters.spec.js (2)
  • payload (173-173)
  • payload (198-198)
src/Auth.js (1)
src/RestWrite.js (1)
  • providerAuthData (492-492)
src/RestWrite.js (1)
src/Auth.js (5)
  • changed (644-644)
  • hasMutatedAuthData (456-470)
  • hasMutatedAuthData (468-468)
  • mutatedAuthData (458-458)
  • user (527-527)
spec/Users.authdata.spec.js (2)
spec/helper.js (1)
  • reconfigureServer (171-205)
src/RestWrite.js (1)
  • authData (462-462)
🔇 Additional comments (16)
src/Auth.js (4)

426-428: Early return optimization looks good!

Good optimization to avoid unnecessary processing when providerAuthData is falsy. This aligns well with the delta-based approach.


430-435: Conditional adapter resolution improves performance.

The refactor to only retrieve the adapter when beforeFind is truthy and the adapter has a beforeFind method is a good optimization that reduces unnecessary adapter lookups.


609-636: Well-implemented subset equality with clear semantics.

The subsetEqual function is well-structured with proper handling of:

  • Primitive types with strict equality
  • Arrays requiring exact length and element-wise equality
  • Objects allowing partial updates (undefined values treated as "not provided")

The implementation correctly supports the delta-based update pattern.


643-671: Excellent delta computation logic!

The diffAuthData function cleanly categorizes providers into changed/unlink/unchanged states. The explicit null check for unlinking and the subset equality check for detecting actual changes are particularly well done.

src/RestWrite.js (6)

158-167: Session token handling for login via authData is appropriate.

The logic to return the user with session during authData login is well-placed and properly checks for the storage flag before modifying the response.


538-540: Good defensive check for authData.id presence.

The early return when no provider has an id field prevents unnecessary database queries.


554-615: Complex but necessary refactor of handleAuthData.

The multi-path login/linking flow properly handles:

  1. User lookup by ID vs authData
  2. Delta computation using the new utilities
  3. Separation of changed/unchanged/unlinked providers

The logic is complex but appears correct for the various scenarios.


696-700: Explicit unlink handling looks correct.

Setting authData[key] = null for unlinked providers ensures they are properly removed from the database without validation.


616-628: Ensure existing multiple-provider tests cover the authDataBeforeFindTriggered optimization
Review spec/Users.authdata.spec.js:81 and spec/ParseUser.spec.js:1740–1780 to confirm that when authDataBeforeFindTriggered is true, edge-case validations (linking a provider already on another user or updating multiple providers simultaneously) still run; if not, add tests.


683-690: No change needed for authData validation order
The early-return (if (!hasMutatedAuthData && !isLogin)) skips validation on updates with unchanged authData, so expired tokens are only validated on login or when authData is mutated, as intended and covered by existing tests.

Likely an incorrect or invalid review comment.

spec/AuthenticationAdaptersV2.spec.js (1)

832-832: Test mutation is appropriate for validation.

The mutation of payload.someData = false between validation calls is a good test to ensure the adapter receives the updated payload on subsequent validations.

spec/Users.authdata.spec.js (5)

31-51: Config helper looks good and self-contained.


57-80: Provider linking flow LGTM.

Clear demonstration of linking a second provider while keeping existing gpgames unchanged.


10-29: Ensure the fetch mock supports multiple calls.

mockGooglePlayGamesAPI may be invoked in tests that trigger two token exchanges (initial + partial update). Confirm the mock doesn’t exhaust its response list.

If it’s single-use, adjust the helper to return responses per URL indefinitely.


1-1: Globals already provided by spec/helper.js
Both reconfigureServer and mockFetch are assigned to global in spec/helper.js (and listed as globals in your ESLint config), so they’re available at runtime without an explicit require.

Likely an incorrect or invalid review comment.


10-29: Mock the /games/v1/players/me endpoint
The GPGames provider calls GET https://www.googleapis.com/games/v1/players/me, so update the test’s mock URL to avoid bypass:

-      {
-        url: `https://www.googleapis.com/games/v1/players/${MOCK_USER_ID}`,
+      {
+        url: 'https://www.googleapis.com/games/v1/players/me',
         method: 'GET',
         response: {
           ok: true,
           json: () => Promise.resolve({ playerId: MOCK_USER_ID }),
         },
       },

coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 5, 2025
Copy link

codecov bot commented Sep 8, 2025

Codecov Report

❌ Patch coverage is 95.55556% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.35%. Comparing base (82fdb0d) to head (7669bd9).

Files with missing lines Patch % Lines
src/Auth.js 88.37% 5 Missing ⚠️
src/RestWrite.js 98.91% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (82fdb0d) and HEAD (7669bd9). Click for more details.

HEAD has 5 uploads less than BASE
Flag BASE (82fdb0d) HEAD (7669bd9)
10 5
Additional details and impacted files
@@            Coverage Diff             @@
##            alpha    #9856      +/-   ##
==========================================
- Coverage   92.99%   84.35%   -8.65%     
==========================================
  Files         187      187              
  Lines       15096    15183      +87     
  Branches      174      174              
==========================================
- Hits        14039    12808    -1231     
- Misses       1045     2363    +1318     
  Partials       12       12              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@Moumouls Moumouls left a comment

Choose a reason for hiding this comment

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

note: not in favor of this kind of refactor on a sensible app aera, the "null" issue to unlink a provider should be performed with minimal changes if possible

src/Auth.js Outdated
@@ -601,6 +606,70 @@ const handleAuthDataValidation = async (authData, req, foundUser) => {
return acc;
};

const subsetEqual = (prev, next) => {
Copy link
Member

Choose a reason for hiding this comment

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

issue: diff and equality system should not be coded from scratch and if not done correctly could be an attack vector, popular libs are native implementation should be used

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the feedback on diff/equality — agreed. I overreached trying to fix too much at once. I’ve reduced this PR to minimal changes only:

Before findUsersWithAuthData, I filter out providers where authData[provider] is null (or undefined), so lookup/validation runs only on non-null providers.

Unlink via authData[provider] = null is applied without invoking the adapter.

If we revisit partial update semantics later, I’ll rely on native or well-vetted utilities and propose that in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

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

Ohh thanks @SNtGog diffs looks much better 🚀

@SNtGog SNtGog changed the title feat(auth): Delta-based authData updates; skip redundant validation; explicit unlink semantics fix(auth): treat authData[provider]=null as unlink; skip provider validation for unlink Sep 8, 2025
Copy link

I will reformat the title to use the proper commit message syntax.

@parse-github-assistant parse-github-assistant bot changed the title fix(auth): treat authData[provider]=null as unlink; skip provider validation for unlink fix(auth): Treat authData[provider]=null as unlink; skip provider validation for unlink Sep 8, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 8, 2025
Copy link

@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: 0

🧹 Nitpick comments (5)
spec/RestWrite.handleAuthData.spec.js (5)

46-66: Harden the test to prove “no provider validation on unlink”

After login, explicitly forbid any further external fetches so unlink would fail if it tried to validate. This removes any ambiguity about leftover mocks.

Apply this diff:

 it('should unlink provider via null', async () => {
-  mockGooglePlayGamesAPI();
+  mockGooglePlayGamesAPI();
   const authData = createMockUser();
   const user = await Parse.User.logInWith('gpgames', { authData });
   const sessionToken = user.getSessionToken();

   await user.fetch({ sessionToken });
   const currentAuthData = user.get('authData') || {};
+
+  // Disallow any external calls during unlink; validation must be skipped.
+  mockFetch([]);

56-59: Set only the provider you want to unlink

Merging in currentAuthData is unnecessary and can hide intent. Setting just { gpgames: null } is sufficient and tighter.

Apply this diff:

-  user.set('authData', {
-    ...currentAuthData,
-    gpgames: null,
-  });
+  user.set('authData', { gpgames: null });

5-9: Use a per-test unique provider user id to avoid cross-spec interference

If other specs reuse the same provider/user id, collisions can occur. Generate a unique id and thread it through the mocks.

Apply this diff and thread userId into the mock + authData:

-const createMockUser = () => ({
-  id: MOCK_USER_ID,
+const uniqueId = () => `gpg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
+const createMockUser = (id = uniqueId()) => ({
+  id,
   code: 'C1',
 });

And update the usages:

-const mockGooglePlayGamesAPI = () => {
+const mockGooglePlayGamesAPI = userId => {
   mockFetch([
     {
       url: 'https://oauth2.googleapis.com/token',
@@
     {
-      url: `https://www.googleapis.com/games/v1/players/${MOCK_USER_ID}`,
+      url: `https://www.googleapis.com/games/v1/players/${userId}`,
-  mockGooglePlayGamesAPI();
-  const authData = createMockUser();
+  const userId = uniqueId();
+  mockGooglePlayGamesAPI(userId);
+  const authData = createMockUser(userId);

42-44: Consider beforeAll for speed (if config remains constant across tests)

Reconfiguring the server per test is heavier. If you add more tests in this suite with the same auth config, switch to beforeAll and, if needed, reset state per test with a user cleanup.


10-29: Optional: strengthen mocks with request assertions

If mockFetch supports validating request bodies/headers, assert expected token-exchange payload (grant_type=authorization_code, code, client_id, client_secret) and HTTP methods. This guards against regressions in adapter wiring without adding noise.

Happy to update the mocks with strict assertions if you share the mockFetch helper’s API.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2979264 and 4e3ac64.

📒 Files selected for processing (1)
  • spec/RestWrite.handleAuthData.spec.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
spec/RestWrite.handleAuthData.spec.js (1)
spec/helper.js (1)
  • reconfigureServer (171-205)
🔇 Additional comments (2)
spec/RestWrite.handleAuthData.spec.js (2)

31-40: Auth config setup looks good

Scoped provider config is clear and minimal.


62-66: Good verification with master key

Asserting authData.gpgames is absent via a master-key read correctly validates persistence-layer unlink.

await user.save(null, { sessionToken });

const updatedUser = await new Parse.Query(Parse.User).get(user.id, { useMasterKey: true });
const finalAuthData = updatedUser.get('authData') || {};
Copy link
Member

@Moumouls Moumouls Sep 9, 2025

Choose a reason for hiding this comment

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

issue: You should not fall back to "{}". In this test, you expect to receive authData. Otherwise, if authData becomes protected in the future, the test could produce false positives.

Additionally, you need to set another authData in this test to ensure that:

  • gpgames is correctly removed without deleting all authData.
  • You can then expect that the other authData entries remain unchanged and that the null operation was correctly performed.

Comment on lines +542 to +547
for (const provider of Object.keys(authData)) {
if (authData[provider] === null || authData[provider] === undefined) {
continue;
}
withoutUnlinked[provider] = authData[provider];
}
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: using dedicated tools and functional prog to perform ops on objects, may be you will learn something

const authDataWithoutNullish = Object.fromEntries(Object.entries(authData).filter([_, data] => data ?? false))

When you want to perform filtering on objects, combining Object.fromEntries + Object.entries + .filter() works well (in case of functional programming)

Copy link
Member

Choose a reason for hiding this comment

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

Object.fromEntries combined with Object.entries is underrated: https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries

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.

Unlink (authData[provider] = null) triggers OAuth code/token validation instead of unlinking
3 participants