Skip to content

Conversation

@Vishal4real
Copy link

@Vishal4real Vishal4real commented Feb 11, 2026

Pull Request

Issue

A logged-in user can currently disable MFA for their account without entering a valid verification code by simply calling the unlink function or saving null for the MFA auth data. This bypasses the intended security requirement of verifying a TOTP/code before removal.

Closes: #9623

Approach

  1. Extended AuthAdapter: Added a new validateUnlink method to the AuthAdapter class to allow adapters to validate removal requests.
  2. Implemented in MFA: Implemented validateUnlink in the MFAAdapter to explicitly reject unlinking attempts (throws "Invalid MFA data"). This closes the loophole where users could remove MFA without a code.
  3. Core Update: Updated Auth.js and Adapters/Auth/index.js to invoke validateUnlink when an auth provider is set to null or unlinked, rather than immediately removing it.

Tasks

Summary by CodeRabbit

  • New Features
    • Enhanced unlink validation for authentication providers: unlink now invokes provider-specific validation, preventing unauthorized MFA removal while allowing master-key driven unlinking.
  • Tests
    • Added tests covering unlink scenarios: rejecting improper MFA unlink attempts and allowing removal when authorized with the master key.

@parse-github-assistant
Copy link

🚀 Thanks for opening this pull request!

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Feb 11, 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 11, 2026

📝 Walkthrough

Walkthrough

Adds a validateUnlink hook for auth providers and uses it when clearing a provider (authData set to null); MFA adapter implements validateUnlink to block unlinking unless allowed (e.g., master key). Tests added to cover unlink behaviors for TOTP and SMS adapters.

Changes

Cohort / File(s) Summary
Tests
spec/AuthenticationAdapters.spec.js
Added tests verifying MFA unlink behavior: TOTP cannot be unlinked via null or _unlinkFrom, SMS can be unlinked with master key; minor test formatting tweaks.
Adapter base & loader
src/Adapters/Auth/AuthAdapter.js, src/Adapters/Auth/index.js
Added new adapter API method validateUnlink(authData, options, req) to the base adapter and included it in adapter loading/normalization so adapters can provide unlink validation.
MFA adapter
src/Adapters/Auth/mfa.js
Implemented validateUnlink to reject unlink attempts unless request has master privileges.
Auth integration
src/Auth.js
When authData for a provider is cleared, attempt to load provider adapter and call its validateUnlink; failure to load adapter falls back to prior behavior with a debug log.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant AuthModule as Auth.js
participant AdapterLoader as loadAuthAdapter
participant MFAAdapter as MFA Adapter
participant DB
Client->>AuthModule: request save(authData: {mfa: null})
AuthModule->>AdapterLoader: load adapter for 'mfa'
AdapterLoader-->>AuthModule: adapter instance (has validateUnlink)
AuthModule->>MFAAdapter: validateUnlink(null, options, req)
MFAAdapter-->>AuthModule: throws error (unless master)
AuthModule--xClient: return validation error
Note right of AuthModule: If adapter missing or no validateUnlink, proceed to clear provider
AuthModule->>DB: update user authData (clear provider)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • mtrezza
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the main security fix: disabling MFA without TOTP verification is now prevented.
Description check ✅ Passed Description includes issue reference, clear approach explanation, and completed tasks. Properly structured per template with all key sections filled.
Linked Issues check ✅ Passed All code changes directly address issue #9623: validateUnlink method added to AuthAdapter and MFAAdapter, core auth logic updated to invoke validation before removing MFA, and tests added to verify the security fix.
Out of Scope Changes check ✅ Passed All changes are scoped to the security objective. Minor formatting tweaks in tests are incidental. No unrelated changes detected.

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

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

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
spec/AuthenticationAdapters.spec.js (2)

1700-1752: Good coverage of both unlink attack vectors.

Both tests correctly verify the fix for the two bypass methods from issue #9623 (setting authData.mfa = null and _unlinkFrom('mfa')). Consider using toBeRejectedWith(...) with the expected error message ('Invalid MFA data') for more precise assertions, but this is optional.


1895-1899: Defensive conditional weakens the assertion.

The if/else means the test passes regardless of whether authData is defined or not. After unlinking a single provider, the authData object should still exist on the user — only authData.mfa should be absent. A direct assertion is more robust:

Suggested fix
-    const authData = user.get('authData');
-    if (authData) {
-      expect(authData.mfa).toBeUndefined();
-    } else {
-      expect(authData).toBeUndefined();
-    }
+    const authData = user.get('authData');
+    expect(authData?.mfa).toBeUndefined();

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.

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 `@src/Adapters/Auth/mfa.js`:
- Around line 194-196: The validateUnlink method currently always throws and
blocks master-key admin recovery; change its signature to accept the
requestObject parameter (matching validateUpdate) and implement the same
master-key bypass logic: if requestObject && requestObject.master then
return/allow the unlink, otherwise keep throwing the 'Invalid MFA data' error;
update the function name reference validateUnlink to reflect the new parameter
usage so admin/master requests can clear MFA.

In `@src/Auth.js`:
- Around line 574-578: The catch around
req.config.authDataManager.getValidatorForProvider(provider) silently swallows
all errors; change it to either narrow the catch to expected errors (e.g., known
"provider not found" type) or log the caught error at debug/verbose level before
continuing so misconfiguration/bundle-load failures are visible; specifically
update the block that assigns authAdapter to handle only the expected
exception(s) or call something like req.logger.debug/error with the exception
message and provider name when the catch fires, leaving behavior unchanged for
known "no validator" cases.
🧹 Nitpick comments (3)
spec/AuthenticationAdapters.spec.js (2)

1700-1726: Good coverage of the null-unlink vector.

Consider using toBeRejectedWith(...) with the expected error to make the assertion more precise and catch regressions where the error message changes unexpectedly.


1728-1752: Good coverage of the _unlinkFrom vector.

Same suggestion as above — toBeRejectedWith(...) would strengthen the assertion. Also consider adding a backward-compatibility test verifying that a non-MFA provider (one without validateUnlink) can still be unlinked as before.

src/Auth.js (1)

573-584: Double lookup of the provider adapter.

getValidatorForProvider(provider) is called here (line 575) and again at line 586 when the adapter does have validateUnlink. Consider reusing the result to avoid loading the adapter twice:

Suggested refactor
       if (authData[provider] === null) {
         let authAdapter;
         try {
           authAdapter = req.config.authDataManager.getValidatorForProvider(provider);
         } catch (e) {
           // Ignore error
         }
         const { adapter } = authAdapter || {};
 
         if (!adapter || typeof adapter.validateUnlink !== 'function') {
           acc.authData[provider] = null;
           continue;
         }
       }
-      const { validator } = req.config.authDataManager.getValidatorForProvider(provider) || {};
+      const { validator } = authAdapter || req.config.authDataManager.getValidatorForProvider(provider) || {};

Note: authAdapter would need to be declared in the outer scope (before the if (authData[provider] === null) block) for this to work.

@mtrezza
Copy link
Member

mtrezza commented Feb 11, 2026

This has been discusses previously (can't find the issue). It is common that MFA can be disabled without entering the MFA code. That does not "bypass the intended security requirement of verifying a TOTP/code before removal", as in order to do this modification, the user needs to be authenticated with MFA in the first place.

If an account security policy requires MFA to be entered before it can be disabled, that is a valid policy decision and it would be good to support that. However, it should not be made a requirement, as not requiring to enter MFA is valid as well.

* @param {Parse.Cloud.TriggerRequest} request
* @returns {Promise<ParseAuthResponse|void|undefined>}
*/
validateUnlink(authData, options, req) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need a new validateUnlink; isn't that a subset of validateUpdate?

Copy link
Author

Choose a reason for hiding this comment

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

It's mainly about not breaking existing code. Most adapters out there expect an actual object in validateUpdate and directly access things like authData.id
if we pass null, those adapters would blow up with TypeErrors.

Copy link
Member

@mtrezza mtrezza Feb 11, 2026

Choose a reason for hiding this comment

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

But if validateUnlink is a subset of validateUpdate, then I don't think we should introduce a validateUnlink, as it seems like an unnecessary adapter interface expansion. It's just about the adapter config then. If existing adapter can't handle adding an option to require or not MFA for unlinking then it sounds like an adapter issue, and may require a new (additional) adapter.

@Vishal4real
Copy link
Author

I agree it should be a policy decision. Currently, Auth.js has a hardcoded bypass that skips validators entirely when authData is null, so developers have no way to enforce a policy if they want to. validateUnlink fixes that bypass and provides the hook so each adapter can decide for itself whether to allow silent unlinking or require additional verification.

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.

Disable MFA without TOTP

3 participants