Skip to content

Conversation

@mtrezza
Copy link
Member

@mtrezza mtrezza commented Feb 7, 2026

Pull Request

Issue

Unlinking auth provider triggers auth data validation.

Closes: #9856
Closes: #9855

Approach

Without the fix, the added test fails:

1) Auth Adapter features should unlink a code-based auth provider without triggering adapter validation
  Message:
    Error: gpgames code is required.
  Stack:
    error properties: Object({ code: 142 })
        at handleError (parse-server/node_modules/parse/lib/node/RESTController.js:327:15)
        at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

This is a significant simplification over #9856.

The change in src/Adapters/Storage/Postgres/PostgresStorageAdapter.js also fixes a bug for Postgres (unrelated to the issue reported in #9855, but apparent with the fix implemented here). The change fixes unlinking auth data in Postgres by properly removing JSON keys instead of setting them to null. Before, when auth data had __op: 'Delete', it set the value to null in the JSONB field using json_object_set_key — the key remained with a null value. After, it uses a new generateRemove helper that uses Postgres's operator (jsonb - key) to actually remove the key from the JSONB object. Both explicit __op: 'Delete' and null values trigger key removal.

Summary by CodeRabbit

  • New Features

    • Support for unlinking code-based authentication providers.
  • Tests

    • Added tests covering unlink flows in multi-provider scenarios, ensuring safe removal without affecting other providers.
  • Improvements

    • Unlink operations no longer trigger external validation or network calls.
    • Auth data updates correctly handle and remove provider credentials during updates.
  • Documentation

    • Clarified adapter validation and after-find behavior.

@parse-github-assistant
Copy link

parse-github-assistant bot commented Feb 7, 2026

🚀 Thanks for opening this pull request!

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Feb 7, 2026

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

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 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

Avoids validating or contacting code-based auth adapters when unlinking providers by treating authData[provider] = null as removal; propagates an isUpdate flag to skip adapter beforeFind during updates, adjusts Postgres JSONB removal, and adds tests confirming no external calls on unlink.

Changes

Cohort / File(s) Summary
Tests
spec/AuthenticationAdaptersV2.spec.js
Added two tests verifying unlinking a code-based provider (authData[provider] = null) does not trigger adapter validation or external HTTP calls and that other providers remain unchanged.
Auth lookup
src/Auth.js
Added isUpdate parameter to findUsersWithAuthData(...); skip processing for providers with null authData and avoid calling beforeFind when echoed-back authData has no credentials in update scenarios.
Rest write flow
src/RestWrite.js
Derives isUpdate (presence of a query) and passes it to Auth.findUsersWithAuthData(...) so auth handling distinguishes create vs update (unlink) paths.
Postgres storage
src/Adapters/Storage/Postgres/PostgresStorageAdapter.js
Added generateRemove() helper and treat __op: 'Delete'/null authData as JSONB removal expressions in updateObjectsByQuery, producing UPDATE statements that remove provider keys.
Auth adapter docs
src/Adapters/Auth/BaseCodeAuthAdapter.js
Added JSDoc comments describing validation flows and data exposure for validateLogin/validateSetUp/validateUpdate/afterFind; behavior unchanged.

Sequence Diagram

sequenceDiagram
    actor Client
    participant RestWrite
    participant Auth as Auth.findUsersWithAuthData
    participant Adapter as BaseCodeAuthAdapter
    participant Storage as PostgresStorageAdapter
    participant DB

    Client->>RestWrite: Request update with authData[provider]=null
    RestWrite->>RestWrite: isUpdate = true (query present)
    RestWrite->>Auth: findUsersWithAuthData(..., isUpdate=true)
    Auth->>Auth: Detect provider authData === null or echoed-back without credentials
    Auth->>Adapter: (skips) beforeFind/validation for removed/empty credentials
    Auth-->>RestWrite: Return no validation lookups for removed provider
    RestWrite->>Storage: updateObjectsByQuery with authData removal instruction
    Storage->>Storage: generateRemove(...) for JSONB key deletion
    Storage->>DB: Execute UPDATE removing provider key from authData
    DB-->>Storage: Ack
    Storage-->>RestWrite: Update complete
    RestWrite-->>Client: Provider unlinked (no external adapter validation)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: fixing a bug where unlinking auth providers triggers validation instead of simply removing them.
Description check ✅ Passed The PR description is complete with Issue, Approach, and explains the fix comprehensively including the related Postgres JSONB bug fix.
Linked Issues check ✅ Passed All code changes directly address the linked issue #9855: preventing authData validation when unlinking by distinguishing between credential updates and null/echo-back scenarios, and skipping beforeFind accordingly.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing unlink auth provider validation: Auth.js query behavior, BaseCodeAuthAdapter documentation, Postgres JSONB deletion, RestWrite flags, and test coverage for unlink scenarios.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 7, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 7, 2026
@codecov
Copy link

codecov bot commented Feb 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.57%. Comparing base (d186471) to head (d875039).

Additional details and impacted files
@@            Coverage Diff             @@
##            alpha   #10045      +/-   ##
==========================================
+ Coverage   83.72%   92.57%   +8.85%     
==========================================
  Files         191      191              
  Lines       15573    15590      +17     
  Branches      176      176              
==========================================
+ Hits        13039    14433    +1394     
+ Misses       2518     1145    -1373     
+ Partials       16       12       -4     

☔ 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.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
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: 2

🤖 Fix all issues with AI agents
In `@spec/AuthenticationAdaptersV2.spec.js`:
- Around line 1449-1452: Test calls to Parse.User.logInWith('gpgames') are
missing the redirect_uri in authData which the gpgames adapter's
getAccessTokenFromCode expects; update the test where user is created (the
logInWith call) to include authData.redirect_uri with the same redirect value
used earlier in the spec (so authData includes id, code, and redirect_uri) to
keep the test consistent with the initial link flow and prevent false positives
if mocks are tightened.
- Line 1411: The test is accidentally using Jasmine's focused runner: replace
the focused test declaration `fit` with a normal test declaration `it` for the
test titled "should unlink one code-based provider while echoing back another
unchanged" so the entire suite runs; locate the `fit('should unlink one
code-based provider while echoing back another unchanged', async () => {` and
change `fit` to `it`.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Auth.js (1)

462-501: ⚠️ Potential issue | 🟡 Minor

hasMutatedAuthData treats empty provider objects as unmutated — add clarifying documentation.

Lines 490-493: The subset check using incomingKeys.some() correctly handles echoed-back data. However, if providerData is an empty object {}, the loop never executes and hasChanges remains false, meaning the provider is silently treated as unmutated. This behavior mirrors the pattern in findUsersWithAuthData (lines 432-436), which explicitly skips authData with "only id or empty" credentials. While this appears intentional for ignoring echoed-back/credential-less updates, the hasMutatedAuthData function lacks an explicit comment or test confirming this edge case. Add a clarifying comment explaining why empty objects are treated as unmutated, or document this behavior in tests.

🧹 Nitpick comments (1)
src/Adapters/Auth/BaseCodeAuthAdapter.js (1)

75-82: Consider clarifying the error message or extracting the shared guard.

The guard if (!authData?.access_token) throws "code is required." — this is technically checking for access_token, not code. While users do submit a code (which beforeFind exchanges for access_token), a developer debugging adapter internals may find this confusing since the validated field doesn't match the message.

Also, the identical guard is copy-pasted across validateLogin, validateSetUp, and validateUpdate.

♻️ Optional: extract shared guard and align the message
+ _requireAccessToken(authData) {
+   if (!authData?.access_token) {
+     throw new Parse.Error(
+       Parse.Error.VALIDATION_ERROR,
+       `${this.adapterName} code is required.`
+     );
+   }
+ }
+
  validateLogin(authData) {
-   if (!authData?.access_token) {
-     throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
-   }
+   this._requireAccessToken(authData);
    return {
      id: authData.id,
    }
  }

  validateSetUp(authData) {
-   if (!authData?.access_token) {
-     throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
-   }
+   this._requireAccessToken(authData);
    return {
      id: authData.id,
    }
  }

  validateUpdate(authData) {
-   if (!authData?.access_token) {
-     throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
-   }
+   this._requireAccessToken(authData);
    return {
      id: authData.id,
    }
  }

Also applies to: 84-91, 99-106

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
@mtrezza
Copy link
Member Author

mtrezza commented Feb 8, 2026

@Moumouls if you want to take a quick look at this, just a sanity check, if you see anything fishy in these changes before we merge...

@mtrezza
Copy link
Member Author

mtrezza commented Feb 9, 2026

@coderabbitai review this PR again for any potential security issues, given the sensitive nature of the changes in authData validation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

🧩 Analysis chain

@mtrezza, I'll conduct a thorough security-focused review of these authData validation changes.


🏁 Script executed:

#!/bin/bash
# Read the modified source files to examine the actual implementation
echo "=== src/Auth.js ==="
cat src/Auth.js

echo -e "\n=== src/RestWrite.js (relevant section) ==="
# Find the handleAuthData method
sed -n '/handleAuthData/,/^  [a-zA-Z]/p' src/RestWrite.js | head -100

echo -e "\n=== src/Adapters/Auth/BaseCodeAuthAdapter.js ==="
cat src/Adapters/Auth/BaseCodeAuthAdapter.js

echo -e "\n=== src/Adapters/Storage/Postgres/PostgresStorageAdapter.js (authData section) ==="
# Find authData handling in updateObjectsByQuery
rg -A 30 "authData.*__op.*Delete" src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Length of output: 26282


🏁 Script executed:

#!/bin/bash
# Get the PostgreSQL authData update section more carefully
echo "=== PostgreSQL authData update handling ==="
rg -n -B 5 -A 20 "generateRemove" src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

echo -e "\n=== Check test file for security-relevant tests ==="
cat spec/AuthenticationAdaptersV2.spec.js | grep -A 50 "should unlink a code-based auth provider"

echo -e "\n=== Check RestWrite handleAuthData full context ==="
rg -n -B 10 -A 40 "handleAuthData = async function" src/RestWrite.js

echo -e "\n=== Check findUsersWithAuthData calls ==="
rg -n "findUsersWithAuthData" src/RestWrite.js

Length of output: 5888


🏁 Script executed:

#!/bin/bash
# Check authorization enforcement in the update flow
echo "=== Check authorization checks in handleAuthData flow ==="
rg -n -A 30 "isCurrentUserLoggedOrMaster" src/RestWrite.js

echo -e "\n=== Check hasMutatedAuthData implementation for security ==="
rg -n -A 25 "^const hasMutatedAuthData" src/Auth.js

echo -e "\n=== Check if there are ACL checks for authData updates ==="
rg -n "filteredObjectsByACL" src/RestWrite.js

echo -e "\n=== Verify test covers security scenarios ==="
rg -n -B 3 -A 15 "should unlink one code-based provider while echoing" spec/AuthenticationAdaptersV2.spec.js

Length of output: 5330


🏁 Script executed:

#!/bin/bash
# Verify the isUpdate flag logic and update vs create detection
echo "=== Check query usage in RestWrite to verify isUpdate logic ==="
rg -n "this\.query\s*=" src/RestWrite.js | head -20

echo -e "\n=== Verify create vs update pathways ==="
rg -n -B 3 -A 3 "new RestWrite" src/Rest.js | head -40

echo -e "\n=== Check if unauthorized users can manipulate authData ==="
rg -n -B 5 -A 10 "validatePermission.*authData" src/RestWrite.js

echo -e "\n=== Verify master key can't be bypassed ==="
rg -n "isMaster" src/RestWrite.js | head -15

echo -e "\n=== Check test for unauthorized unlink attempt ==="
rg -n -B 2 -A 20 "should not.*unlink.*another user|unauthorized.*unlink" spec/AuthenticationAdaptersV2.spec.js

Length of output: 1426


🏁 Script executed:

#!/bin/bash
# Check filteredObjectsByACL implementation for security
echo "=== Check filteredObjectsByACL implementation ==="
rg -n -B 5 -A 20 "^RestWrite.prototype.filteredObjectsByACL" src/RestWrite.js

echo -e "\n=== Check if there are tests for unauthorized authData manipulation ==="
rg -n -C 5 "ACCOUNT_ALREADY_LINKED" spec/AuthenticationAdaptersV2.spec.js

echo -e "\n=== Check for session-based authorization in RestWrite ==="
rg -n "sessionToken" src/RestWrite.js | head -10

echo -e "\n=== Verify RestWrite constructor and query parameter ==="
rg -n -A 15 "^function RestWrite" src/RestWrite.js

echo -e "\n=== Check Routes.js for how RestWrite is instantiated ==="
fd -e js Routes.js -x cat {}

Length of output: 2069


🏁 Script executed:

#!/bin/bash
# Check how update vs create is determined and authorized
echo "=== Check create vs update in routers ==="
rg -n "new RestWrite" src/routers/*.js | head -30

echo -e "\n=== Check if ACL permissions are properly enforced for updates ==="
rg -n -B 3 -A 10 "canUpdateField.*authData" src/RestWrite.js

echo -e "\n=== Examine validateClientClassCreation for authData ==="
rg -n -B 5 -A 10 "validateClientClassCreation" src/RestWrite.js | head -40

echo -e "\n=== Check if there are any bypass scenarios in handleAuthData ==="
rg -n -B 2 -A 2 "master.*authData|authData.*master" src/RestWrite.js

echo -e "\n=== Look at actual test to understand the security scenario ==="
sed -n '1350,1450p' spec/AuthenticationAdaptersV2.spec.js

Length of output: 4620


@mtrezza, I've completed a comprehensive security review of the authData validation changes. Here's my analysis:

✅ Security Controls Verified

1. Authorization Enforcement
The code properly restricts authData modifications to authorized users only:

  • Line 544-545: Updates are correctly detected via isUpdate = !!this.query
  • Line 580-582: isCurrentUserLoggedOrMaster check ensures only the account owner or master key can modify authData
  • Line 586: Operations only proceed if authorized
  • Line 546: Results are filtered by ACL before any authData processing

Result: Unauthorized users cannot unlink providers from other users' accounts.

2. Unlink Operation Detection
Null value handling is secure:

  • Line 420-423 in Auth.js: Providers with null value are identified early and skip validation
  • Line 473-475: hasMutatedAuthData correctly recognizes null as an unlink mutation
  • handleAuthDataValidation properly handles null providers (skips validation)

Result: Unlink operations are correctly differentiated from link/update operations.

3. Credential vs Echo-Back Detection
The hasCredentials check prevents validation bypass:

  • Line 417-418: Detects if authData contains keys other than 'id'
  • Line 422-426: Only skips beforeFind validation when no credentials present AND it's an update
  • This correctly handles the afterFind scenario where only {id: 'x'} is returned to client

Result: Echoing back existing authData doesn't trigger unnecessary validation, but actual credential changes do.

4. Cross-User Attack Prevention
Even if an attacker attempts to link to another user's provider ID:

  • The database query finds the other user (line 428-430)
  • Authorization check fails at line 550: foundUserIsNotCurrentUser
  • Throws ACCOUNT_ALREADY_LINKED error (line 556)

Result: Cannot hijack another user's authentication provider.

5. Database Layer Security
PostgreSQL adapter properly handles authData deletion:

  • Lines 1619-1620: Treats __op: 'Delete' as null
  • Line 1623-1626: Uses generateRemove for safe JSONB key removal
  • No SQL injection vectors introduced

Result: Database operations are secure.

🟡 Minor Observations (Not Security Issues)

1. isUpdate Flag Simplicity
The isUpdate = !!this.query logic is straightforward but relies on query being set correctly during RestWrite construction. Based on the constructor (line 31, 78), this appears correct for all update scenarios.

2. filteredObjectsByACL Implementation
Lines 496-507 show a basic ACL filter that removes locked accounts (empty ACL). The full ACL permission check happens via the authorization check at line 580-582, so this is appropriate.

3. Test Coverage
The new tests verify:

  • Unlinking doesn't trigger adapter validation
  • No external API calls during unlink
  • Multi-provider scenarios work correctly
  • However, no explicit test for unauthorized unlink attempts (though existing test coverage likely handles this)

🔒 Security Conclusion

No security vulnerabilities identified. The changes properly:

  • Maintain authorization controls
  • Prevent validation bypass
  • Handle unlink operations securely
  • Protect against cross-user attacks
  • Preserve database integrity

The fix correctly addresses the original issue (unlink triggering validation) while maintaining security boundaries. The isUpdate flag and hasCredentials check work together to distinguish between legitimate echo-back scenarios and actual authentication changes.

The sensitive nature of authData validation is well-handled through multiple layers of security:

  1. ACL filtering
  2. Authorization checks
  3. Credential validation (when appropriate)
  4. Database-level integrity

Recommendation: Merge approved from a security perspective. 🎯

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression where unlinking an auth provider via authData[provider] = null incorrectly triggers adapter validation (notably for OAuth code-based adapters), and aligns Postgres authData updates with the intended “remove key” semantics.

Changes:

  • Pass an “is update” signal into authData user-lookup to skip beforeFind for echoed-back { id } authData during updates and skip unlinked (null) providers.
  • Update Postgres JSONB authData updates to remove provider keys (instead of leaving null values) for both null and __op: 'Delete'.
  • Add specs that ensure unlinking doesn’t trigger external adapter/network validation and preserves other linked providers.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/RestWrite.js Treats authData handling as update vs login/signup when calling authData lookup.
src/Auth.js Skips beforeFind for unlinked providers and for echoed-back { id } authData during updates.
src/Adapters/Storage/Postgres/PostgresStorageAdapter.js Removes JSONB authData keys on unlink instead of setting them to null.
src/Adapters/Auth/BaseCodeAuthAdapter.js Adds/updates documentation comments around validation hooks for code-based adapters.
spec/AuthenticationAdaptersV2.spec.js Adds tests for unlink behavior without triggering network validation and preserving other providers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 75 to 118
/**
* Validates auth data on login. No credential check is needed as `beforeFind`
* already validated credentials before this method is called. `beforeFind`
* exchanges the auth code for an access_token and verifies user identity; it
* rejects requests with missing credentials before these methods are called.
*/
validateLogin(authData) {
// User validation is already done in beforeFind
return {
id: authData.id,
}
}

/**
* Validates auth data on first setup. No credential check is needed as `beforeFind`
* already validated credentials before this method is called. `beforeFind`
* exchanges the auth code for an access_token and verifies user identity; it
* rejects requests with missing credentials before these methods are called.
*/
validateSetUp(authData) {
// User validation is already done in beforeFind
return {
id: authData.id,
}
}

/**
* Returns the auth data to expose to the client after a query.
*/
afterFind(authData) {
return {
id: authData.id,
}
}

/**
* Validates auth data on update. No credential check is needed as `beforeFind`
* already validated credentials before this method is called. `beforeFind`
* exchanges the auth code for an access_token and verifies user identity; it
* rejects requests with missing credentials before these methods are called.
*/
validateUpdate(authData) {
// User validation is already done in beforeFind
return {
id: authData.id,
}

}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

These docs state that validateLogin/validateSetUp/validateUpdate never need credential checks because beforeFind already validated them, but Auth.findUsersWithAuthData now conditionally skips beforeFind on updates for { id } payloads. To avoid accepting unvalidated updates/linking for code-based providers, add explicit credential/token requirements in these validate* methods (or adjust the docs to reflect the actual guarantees).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai evaluate this feedback.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the Auth.js implementation to understand when beforeFind is skipped
# and when validateSetUp/validateUpdate are called
cat src/Auth.js | grep -A 50 "findUsersWithAuthData"

Length of output: 1910


🏁 Script executed:

#!/bin/bash
# Check the RestWrite.js logic for hasMutatedAuthData to understand when validation is triggered
cat src/RestWrite.js | grep -B 5 -A 30 "hasMutatedAuthData"

Length of output: 2925


🏁 Script executed:

#!/bin/bash
# Look for where validateSetUp and validateUpdate are called in the codebase
rg -n "validateSetUp\|validateUpdate" -A 3 -B 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where handleAuthDataValidation is implemented and how it calls validate methods
cat src/Auth.js | grep -A 100 "handleAuthDataValidation"

Length of output: 3427


🏁 Script executed:

#!/bin/bash
# Search for all validate method calls in Auth.js
rg -n "validate(Login|SetUp|Update|Options)" src/Auth.js -A 2 -B 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the full file content to understand the actual implementation
cat src/Adapters/Auth/BaseCodeAuthAdapter.js | grep -A 10 "validateSetUp\|validateUpdate\|validateLogin"

Length of output: 932


🏁 Script executed:

#!/bin/bash
# Find how the validator function is constructed - search in AuthDataManager
cat src/Adapters/AuthDataManager.js | head -200

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Search for where validateLogin, validateSetUp, validateUpdate are invoked
rg -n "\.validateLogin\(|\.validateSetUp\(|\.validateUpdate\(" -A 2 -B 2

Length of output: 5100


🏁 Script executed:

#!/bin/bash
# Check how hasMutatedAuthData determines if authData changed
cat src/Auth.js | grep -A 30 "const hasMutatedAuthData"

Length of output: 2645


🏁 Script executed:

#!/bin/bash
# Look at the validator selection logic in src/Adapters/Auth/index.js
cat src/Adapters/Auth/index.js | grep -B 10 -A 40 "validateUpdate\|validateSetUp\|validateLogin" | head -100

Length of output: 3298


@mtrezza Copilot's feedback is valid and important. There's a critical discrepancy between the documentation added in this PR and the actual behavior.

The Issue

The new documentation states:

"No credential check is needed as beforeFind already validated credentials before this method is called."

However, this is not always true with the changes in Auth.js. Here's the problematic flow:

Attack Scenario 1: Changing provider ID without credentials

  1. User is logged in and has gpgames linked: { id: 'user123', access_token: 'secret' }
  2. After a query, afterFind returns: { id: 'user123' } (credentials stripped)
  3. Attacker modifies the id and saves: { gpgames: { id: 'victim456' } } (no credentials)
  4. hasMutatedAuthData detects the id change → returns true
  5. findUsersWithAuthData is called with isUpdate=true:
    • hasCredentials = false (only { id } field)
    • beforeFind is SKIPPED (line 18-22 in Auth.js)
  6. handleAuthDataValidation calls validateUpdate({ id: 'victim456' })
  7. With the current code: validateUpdate has the credential check → throws error ✅
  8. If docs were accurate: validateUpdate would have no check → accepts invalid data ❌

Attack Scenario 2: Linking a new provider with just an ID

  1. User is logged in, doesn't have gpgames configured
  2. Attacker sends: { gpgames: { id: 'victim789' } }
  3. hasMutatedAuthData returns true (new provider)
  4. beforeFind is SKIPPED (no credentials, isUpdate=true)
  5. validateSetUp({ id: 'victim789' }) is called
  6. Credential check is essential here to reject this attempt

The Root Cause

The docs assume beforeFind always runs before validate* methods, but:

// Auth.js lines 18-22
if (hasCredentials || !isUpdate) {
  await adapter.beforeFind(providerAuthData);
}

On updates, when authData contains only { id } (no credentials), beforeFind is skipped. If that id value has been modified or it's a new provider link, the validate* methods must perform credential checks because beforeFind never ran.

Recommendation

The credential checks in validateSetUp and validateUpdate are essential security controls. The documentation should be updated to reflect this:

/**
 * Validates auth data on first setup. On signup, `beforeFind` always runs first
 * and validates credentials. On update (linking a new provider), `beforeFind`
 * may be skipped when authData has no credentials, so an explicit credential
 * check is required here.
 */
validateSetUp(authData) {
  if (!authData?.access_token) {
    throw new Parse.Error(Parse.Error.VALIDATION_ERROR, `${this.adapterName} code is required.`);
  }
  return { id: authData.id }
}

(This is exactly what the current file_content shows, so the code is correct—only the diff's documentation is misleading.)


🧬 Code Graph Analysis Results

src/Adapters/Auth/BaseCodeAuthAdapter.js
[No direct code snippet from this file is included; only context to aid review.]

spec/Adapters/Auth/BaseCodeAdapter.spec.js 1-5 ```js const BaseAuthCodeAdapter = require('../../../lib/Adapters/Auth/BaseCodeAuthAdapter').default; const adapterName = 'TestAdapter'; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 56-56 ```js const options = { enableInsecureAuth: true }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 65-65 ```js const authData = { access_token: 'validAccessToken' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 74-74 ```js const authData = {}; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 83-83 ```js const authData = { id: 'invalidUserId', access_token: 'validAccessToken' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 92-92 ```js const authData = { code: 'validCode' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 103-103 ```js const authData = { id: 'validUserId', access_token: 'validAccessToken' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 153-153 ```js const authData = { id: 'validUserId' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 161-161 ```js const authData = { id: 'validUserId', access_token: 'validAccessToken' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 167-167 ```js const authData = { id: 'validUserId' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 174-174 ```js const authData = { id: 'validUserId' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 182-182 ```js const authData = { id: 'validUserId', access_token: 'validAccessToken' }; ``` spec/Adapters/Auth/BaseCodeAdapter.spec.js 188-188 ```js const authData = { id: 'validUserId' }; ```

🧠 Learnings used
Learnt from: mtrezza
Repo: parse-community/parse-server PR: 0
File: :0-0
Timestamp: 2025-11-08T13:46:04.940Z
Learning: For new Parse Server options, verify that the option is documented in src/Options/index.js and that npm run definitions has been executed to reflect changes in src/Options/docs.js and src/Options/Definitions.js. README.md documentation is a bonus but not required for new options.

Comment on lines +1490 to +1494
// Unlink gpgames while echoing back instagram unchanged — the common client pattern:
// fetch current state, spread it, set the one to unlink to null
user.set('authData', { ...currentAuthData, gpgames: null });
await user.save(null, { sessionToken });

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The new tests cover unlinking without network calls, but they don't cover the security-sensitive negative case where a client attempts to link (or change) a code-based provider during update using only { id } (no code/access_token). Adding a test that such an update is rejected would help prevent regressions with the new beforeFind-skipping behavior.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai evaluate this feedback.

Copy link

Choose a reason for hiding this comment

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

@mtrezza, Copilot's feedback is valid in principle — security-sensitive negative test cases are crucial for preventing regressions. However, the suggested tests already exist in this PR!

The diff shown here is truncated, but examining the full file content reveals the PR includes three additional tests beyond the two visible in this hunk:

  1. Line 1511: "should reject updating a code-based provider with only an id and no credentials" — verifies that attempting to change a linked code-based provider's ID without providing credentials (code/access_token) is rejected with a validation error.

  2. Line 1537: "should reject linking a new code-based provider with only an id and no credentials" — ensures that initial linking attempts with only { id } are rejected, preventing attackers from linking to another user's provider.

  3. Line 1558: "should handle multiple providers: add one while another remains unchanged (code-based)" — confirms that adding a new provider while echoing back an existing code-based provider (without credentials) works correctly.

Verdict: The test coverage is comprehensive and already addresses Copilot's security concerns. The PR includes both positive cases (successful unlink operations) and negative cases (rejected credential-less updates/links).


🧠 Learnings used
Learnt from: Moumouls
Repo: parse-community/parse-server PR: 9883
File: spec/CloudCodeLogger.spec.js:410-412
Timestamp: 2025-10-16T19:27:05.311Z
Learning: In spec/CloudCodeLogger.spec.js, the test "should log cloud function triggers using the silent log level" (around lines 383-420) is known to be flaky and requires the extra `await new Promise(resolve => setTimeout(resolve, 100))` timeout after awaiting `afterSavePromise` for reliability, even though it may appear redundant.

Comment on lines +432 to +441
const providerKeys = Object.keys(providerAuthData || {});
const hasCredentials = providerKeys.some(key => key !== 'id');

// On update, skip beforeFind for echoed-back authData (no credentials to process).
// On login/signup, always call beforeFind so it can reject missing credentials.
const adapter = config.authDataManager.getValidatorForProvider(provider)?.adapter;
if (beforeFind && typeof adapter?.beforeFind === 'function') {
await adapter.beforeFind(providerAuthData);
if (hasCredentials || !isUpdate) {
await adapter.beforeFind(providerAuthData);
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

On update, skipping adapter.beforeFind when authData only contains { id } allows code-based providers (BaseCodeAuthAdapter) to be linked or have their id changed without any credential/code/token validation (because validateSetUp/validateUpdate just return { id }). This is a security regression: a client could set authData[provider] = { id: 'victim' } during an update and bypass the OAuth code/token exchange. Consider enforcing credential presence for code-based adapters during validation (e.g., require access_token produced by beforeFind), or tighten the skip condition so beforeFind is only skipped when the incoming provider is confirmed unchanged (same provider already linked on the current user with the same id).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai evaluate this feedback.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find ACCOUNT_ALREADY_LINKED error in the codebase
rg -n "ACCOUNT_ALREADY_LINKED" -C 5

# Get the full RestWrite.handleAuthData method implementation
ast-grep --pattern $'handleAuthData($$$) {
  $$$
}'

# Also search for the handleAuthData method more broadly
rg -n "handleAuthData" -A 50 src/RestWrite.js | head -200

Length of output: 11316


🏁 Script executed:

#!/bin/bash
# Look for any validation around changing authData id specifically
rg -n "authData.*id" -C 3 src/Auth.js | head -100

# Check if there are tests for changing/updating provider id
rg -n "change.*id|update.*id|different.*id" spec/AuthenticationAdaptersV2.spec.js spec/ParseUser.spec.js -C 2 | head -150

# Look at hasMutatedAuthData more closely
ast-grep --pattern $'const hasMutatedAuthData = ($$$) => {
  $$$
}'

Length of output: 7609


🏁 Script executed:

#!/bin/bash
# Get the full test that attempts to change gpgames id
rg -n "Attempt to change gpgames id" -A 10 -B 5 spec/AuthenticationAdaptersV2.spec.js

# Also look for the test name/description for this test
rg -n "it\(" spec/AuthenticationAdaptersV2.spec.js | grep -A 1 -B 1 "1548"

# Get more context around line 1548
sed -n '1540,1560p' spec/AuthenticationAdaptersV2.spec.js

Length of output: 1491


@mtrezza, Copilot has identified a valid security concern. Let me trace through the attack scenario:

The Attack


🏁 Script executed:

#!/bin/bash
# Get the BaseCodeAuthAdapter implementation to see what validateUpdate actually does
cat src/Adapters/Auth/BaseCodeAuthAdapter.js

Length of output: 4065


@mtrezza, Copilot has identified an important architectural concern, though the immediate attack is currently blocked. Let me explain:

Current Defense

The attack scenario Copilot describes (user sends { id: 'victimId' } to hijack another provider) is currently blocked by validateUpdate:

Attack flow with PR changes:

  1. Attacker sends: authData.gpgames = { id: 'differentId' } (no credentials)
  2. hasCredentials = false (only id field present)
  3. beforeFind skipped (new behavior: line 439)
  4. hasMutatedAuthData detects id change → validation triggered
  5. validateUpdate called with { id: 'differentId' }
  6. validateUpdate requires access_token (BaseCodeAuthAdapter.js line 108)
  7. No access_token present → throws "code is required"

The test at line 1548 confirms this works as expected.

The Architectural Issue

However, Copilot raises a valid design concern:

Before PR: Security enforcement in one place

  • beforeFind ALWAYS runs and validates credentials
  • Single chokepoint for security

After PR: Security enforcement split across two methods

  • beforeFind skipped when hasCredentials=false
  • validateUpdate must catch credential-less mutations
  • Requires ALL adapters to implement validateUpdate correctly

The Problem with hasCredentials

The hasCredentials check doesn't distinguish between:

  • Safe echo-back: { id: 'A' } when stored data has { id: 'A', access_token: 'xyz' } → no change
  • ⚠️ Attempted hijack: { id: 'B' } when stored data has { id: 'A', ... } → id changed

Both cases have hasCredentials=false, but only the first should skip beforeFind.

Recommended Fix

Instead of checking hasCredentials, check if the provider data is truly unchanged:

// Check if this is genuinely echoed-back data (no actual changes)
const isEchoBack = isUpdate && userAuthData?.[provider] && 
  !hasMutatedAuthData({ [provider]: providerAuthData }, { [provider]: userAuthData[provider] }).hasMutatedAuthData;

if (beforeFind && typeof adapter?.beforeFind === 'function') {
  if (!isEchoBack) {  // Only skip if truly unchanged
    await adapter.beforeFind(providerAuthData);
  }
}

This way:

  • Echo-back of unchanged data → beforeFind skipped ✅
  • Changed id without credentials → beforeFind runs → throws "code is required" ✅
  • Defense-in-depth maintained (both beforeFind and validateUpdate protect)

Would you like me to search for how userAuthData can be accessed at this point in findUsersWithAuthData to implement this fix?


🧬 Code Graph Analysis Results

[src/Adapters/Auth/index.js] 41-67

const providers = {
  apple,
  gcenter,
  gpgames,
  facebook,
  instagram,
  linkedin,
  meetup,
  mfa,
  google,
  github,
  twitter,
  spotify,
  anonymous,
  digits,
  janrainengage,
  janraincapture,
  line,
  vkontakte,
  qq,
  wechat,
  weibo,
  phantauth,
  microsoft,
  keycloak,
  ldap,
};

[spec/Auth.spec.js] 4-4

const { Auth, getAuthForSessionToken } = require('../lib/Auth.js');

[src/triggers.js] 258-311

export function getRequestObject(
  triggerType,
  auth,
  parseObject,
  originalParseObject,
  config,
  context,
  isGet
) {
  const request = {
    triggerName: triggerType,
    object: parseObject,
    master: false,
    log: config.loggerController,
    headers: config.headers,
    ip: config.ip,
    config,
  };

  if (isGet !== undefined) {
    request.isGet = !!isGet;
  }

  if (originalParseObject) {
    request.original = originalParseObject;
  }
  if (
    triggerType === Types.beforeSave ||
    triggerType === Types.afterSave ||
    triggerType === Types.beforeDelete ||
    triggerType === Types.afterDelete ||
    triggerType === Types.beforeLogin ||
    triggerType === Types.afterLogin ||
    triggerType === Types.beforePasswordResetRequest ||
    triggerType === Types.afterFind
  ) {
    // Set a copy of the context on the request object.
    request.context = Object.assign({}, context);
  }

  if (!auth) {
    return request;
  }
  if (auth.isMaster) {
    request['master'] = true;
  }
  if (auth.user) {
    request['user'] = auth.user;
  }
  if (auth.installationId) {
    request['installationId'] = auth.installationId;
  }
  return request;
}

[src/triggers.js] 671-695

export function resolveError(message, defaultOpts) {
  if (!defaultOpts) {
    defaultOpts = {};
  }
  if (!message) {
    return new Parse.Error(
      defaultOpts.code || Parse.Error.SCRIPT_FAILED,
      defaultOpts.message || 'Script failed.'
    );
  }
  if (message instanceof Parse.Error) {
    return message;
  }

  const code = defaultOpts.code || Parse.Error.SCRIPT_FAILED;
  // If it's an error, mark it as a script failed
  if (typeof message === 'string') {
    return new Parse.Error(code, message);
  }
  const error = new Parse.Error(code, message.message || message);
  if (message instanceof Error) {
    error.stack = message.stack;
  }
  return error;
}

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 9, 2026
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

2 participants