Skip to content

CMM-1256: Fix 401 errors for self-hosted sites with application passwords#22603

Merged
adalpari merged 6 commits intotrunkfrom
adalpari/fix-media-401-auth
Feb 18, 2026
Merged

CMM-1256: Fix 401 errors for self-hosted sites with application passwords#22603
adalpari merged 6 commits intotrunkfrom
adalpari/fix-media-401-auth

Conversation

@adalpari
Copy link
Copy Markdown
Contributor

@adalpari adalpari commented Feb 17, 2026

Description

Fixes 401 errors when making GET requests to media and editor settings endpoints on self-hosted WordPress sites that use application passwords. The issue affected both the React Native Gutenberg editor and EditorSettingsStore when fetching settings.

Root cause: ReactNativeStore used cookie-nonce authentication for all non-WPCom sites by attempting to log in via wp-login.php. However, sites using application passwords cannot authenticate via wp-login.php—application passwords only work with REST API Basic authentication. This caused nonce requests to fail silently, and subsequent API requests were sent without authentication headers, resulting in 401 errors.

Solution: Added support for Basic authentication headers in the ReactNativeStore request pipeline. When a site has application password credentials (apiRestUsernamePlain and apiRestPasswordPlain), the store now uses Basic auth directly instead of attempting the nonce flow. This bypasses wp-login.php entirely and uses the credentials in the format that application passwords expect.

Implementation details:

  • Added headers parameter throughout the request pipeline (WPAPIGsonRequestBuilder → ReactNativeWPAPIRestClient → ReactNativeStore)
  • Detects app password sites via site.hasApplicationPassword() check
  • Builds Basic auth header using okhttp3.Credentials.basic() from the site's application password credentials
  • Skips nonce fetch and 401 retry logic for app password sites (not applicable)
  • Keeps 404 error handling for REST URL discovery for both auth methods
  • Backward compatible: all existing callers unaffected (empty headers map by default)

Testing instructions

Test on self-hosted WordPress site with application passwords configured:

  1. Create a self-hosted WordPress site (or use existing)
  2. Configure an application password on the site
  3. In the Android app, log in to the self-hosted site using the application password
  4. Open the editor
  5. Add images and save
  6. Verify media list loads without 401 errors
  7. Verify editor settings load without 401 errors
  8. Create/edit a post and verify media upload works

Test on non-app-password sites (nonce auth):

  1. Log in to self-hosted site without application passwords
  2. Verify editor and media functionality works as before
  3. Verify nonce-based requests still function correctly

Test on WP.com sites:

  • Verify bearer token auth still works unchanged

@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Feb 17, 2026

1 Warning
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@adalpari adalpari changed the title Fix 401 errors for self-hosted sites with application passwords CMM-1256: Fix 401 errors for self-hosted sites with application passwords Feb 17, 2026
@adalpari
Copy link
Copy Markdown
Contributor Author

@claude

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Feb 17, 2026

Claude finished @adalpari's task in 1m 49s —— View job


Code Review Complete ✅

Tasks

  • Read changed files and understand implementation
  • Review code quality and architecture
  • Review security considerations
  • Review performance implications
  • Review test coverage
  • Review documentation
  • Provide comprehensive feedback

Summary

This PR effectively solves the 401 authentication issue for self-hosted sites using application passwords. The implementation is clean, well-tested, and maintains backward compatibility. The changes follow a clear architectural pattern by threading headers through the request pipeline.

✅ Strengths

  1. Clean architecture: Headers are threaded through the request pipeline in a logical way (Store → Client → RequestBuilder)
  2. Strong test coverage: Added 3 comprehensive tests covering GET, POST, and 401 handling for app password sites
  3. Backward compatibility: Default empty maps ensure existing callers are unaffected
  4. Security: Proper use of okhttp3.Credentials.basic() for encoding credentials
  5. Clear separation of concerns: Application password flow is cleanly separated from nonce flow

🔍 Issues Found

1. Security: Potential credential exposure in logs (High Priority)

Location: ReactNativeStore.kt:264-266

The code directly passes plain credentials to Credentials.basic(). While this is correct, if there's any logging in the network layer or if exceptions occur, these could potentially expose credentials.

Recommendation: Verify that the network layer doesn't log headers, especially Authorization headers. Consider adding a code comment warning about this.

// SECURITY: Ensure network layer does not log Authorization header
val authHeaderValue = Credentials.basic(
    site.apiRestUsernamePlain, site.apiRestPasswordPlain
)

2. Code Quality: Missing null safety check (Medium Priority)

Location: ReactNativeStore.kt:264-266

The hasApplicationPassword() method checks for null credentials (SiteModel.java:889-891), but then the code immediately accesses site.apiRestUsernamePlain and site.apiRestPasswordPlain without null-assertion operators or safe calls. While the logic guarantees non-null, Kotlin's type system doesn't know this.

Current code:

val authHeaderValue = Credentials.basic(
    site.apiRestUsernamePlain, site.apiRestPasswordPlain
)

Issue: If hasApplicationPassword() implementation changes or if there's a race condition, this could throw NPE.

Recommendation: Use !! operators to make the non-null assumption explicit, or better yet, use safe calls with error handling:

val authHeaderValue = Credentials.basic(
    site.apiRestUsernamePlain!!, // Safe due to hasApplicationPassword() check
    site.apiRestPasswordPlain!!
)

Or more defensively:

val username = site.apiRestUsernamePlain
val password = site.apiRestPasswordPlain
if (username == null || password == null) {
    // This shouldn't happen, but handle defensively
    return Error(BaseNetworkError(GenericErrorType.UNKNOWN).apply {
        message = "Application password credentials missing"
    })
}
val authHeaderValue = Credentials.basic(username, password)

3. Documentation: Insufficient inline comments (Low Priority)

Location: ReactNativeStore.kt:185-191

The early return for application password sites is a critical code path but lacks explanation of why this bypasses nonce authentication.

Recommendation: Add a comment explaining the rationale:

// Use Basic auth for sites with application passwords instead of nonce auth.
// Application passwords authenticate directly via REST API Basic auth and cannot
// authenticate via wp-login.php (which is required for nonce-based auth).
if (site.hasApplicationPassword()) {
    return executeWithApplicationPassword(...)
}

4. Code Quality: Parameter order inconsistency (Low Priority)

Location: WPAPIGsonRequestBuilder.kt:86 and WPAPIGsonRequestBuilder.kt:123

The headers parameter is added as the last parameter with a default value, which is good. However, in syncPutRequest (line 54-62) and syncDeleteRequest (line 64-72), the headers parameter is missing entirely, which creates inconsistency.

Recommendation: For consistency and future-proofing, add headers parameter to syncPutRequest and syncDeleteRequest as well, even if they're not currently used by ReactNativeStore.

💡 Minor Suggestions

5. Test Coverage: Edge case not tested

Location: Test file ReactNativeStoreWPAPITest.kt

The tests cover the happy path for app password auth, but don't test the 404 retry logic for app password sites (which does exist in the implementation at ReactNativeStore.kt:280-291).

Recommendation: Add a test case:

@Test
fun `site with application password retries on 404 after discovery`() = test {
    // Test that 404 error triggers discovery and retry for app password sites
}

6. Code Clarity: Magic string could be a constant

Location: ReactNativeStore.kt:267 and ReactNativeStore.kt:359

The string "Authorization" appears twice (once as a constant, once used). Good that it's a constant, but the constant is defined in the companion object far from where it's instantiated.

Current: Constant at line 359, used at line 267.

This is actually fine as-is, but for reviewers scanning the code, it might be clearer to reference the constant explicitly in the function signature or local scope. Not a strong suggestion, current approach is acceptable.

🎯 Performance Considerations

No performance concerns identified. The implementation:

  • Avoids unnecessary nonce requests for app password sites (optimization)
  • Uses the same network request flow (no additional overhead)
  • Maintains caching behavior for GET requests

📋 Testing Assessment

Test coverage is good with three dedicated tests:

  1. GET request with Basic auth ✅
  2. POST request with Basic auth ✅
  3. No nonce retry on 401 for app password sites ✅

Missing test case: 404 retry logic for app password sites (mentioned in issue #5 above)

📝 Documentation Assessment

PR description is excellent: Clear explanation of root cause, solution, and implementation details.

Code documentation: Could be improved with inline comments explaining the authentication flow branching (mentioned in issue #3 above).

No README or external docs need updating: This is an internal implementation detail.


Verdict: ✅ Approved with Minor Recommendations

This is a solid implementation that solves the stated problem effectively. The issues identified are mostly minor defensive coding suggestions and documentation improvements. The core logic is sound and well-tested.

Recommended actions before merge:

  1. Address the null safety check (issue Implementing WordPress.com Notifications Activities #2) - this is the most important
  2. Add clarifying comments (issue WordPress.com Notifications in Android #3)
  3. Consider adding the 404 retry test case (issue Fix for bug #403 (new patch) #5)

Nice to have:


@adalpari
Copy link
Copy Markdown
Contributor Author

Addressed Claude comment here: 5e7098f

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Feb 17, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
FlavorJalapeno
Build TypeDebug
Versionpr22603-9da8188
Build Number1484
Application IDcom.jetpack.android.prealpha
Commit9da8188
Installation URL2a7smtu48lh58
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Feb 17, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
FlavorJalapeno
Build TypeDebug
Versionpr22603-9da8188
Build Number1484
Application IDorg.wordpress.android.prealpha
Commit9da8188
Installation URL2nkn7gmp6ehdg
Note: Google Login is not supported on these builds.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 17, 2026

Codecov Report

❌ Patch coverage is 62.29508% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.23%. Comparing base (9c70c06) to head (9da8188).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
.../wordpress/android/fluxc/store/ReactNativeStore.kt 66.66% 11 Missing and 2 partials ⚠️
...luxc/network/rest/wpapi/WPAPIGsonRequestBuilder.kt 33.33% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22603      +/-   ##
==========================================
+ Coverage   38.22%   38.23%   +0.01%     
==========================================
  Files        2246     2246              
  Lines      112669   112717      +48     
  Branches    15688    15695       +7     
==========================================
+ Hits        43066    43096      +30     
- Misses      66026    66041      +15     
- Partials     3577     3580       +3     

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

@nbradbury
Copy link
Copy Markdown
Contributor

@adalpari Claude is flagging the removal of the nonce unknown test as an issue. I added it back locally and it passes. What was the reason for its removal?

Copy link
Copy Markdown
Member

@dcalhoun dcalhoun left a comment

Choose a reason for hiding this comment

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

From a testing perspective, this worked well for me. The changes fixed the original issue. I did not encounter regressions when testing various site types, with both Gutenberg Mobile and GutenbergKit.

Test on non-app-password sites (nonce auth):

I was unable to create a new site without an application password. Is this still possible?

I attempted doing so on a fresh app install without a WP.com account logged in.

@nbradbury
Copy link
Copy Markdown
Contributor

From a testing perspective, this worked well for me. The changes fixed the original issue. I did not encounter regressions when testing various site types, with both Gutenberg Mobile and GutenbergKit.

Same here. I'm okay if we approve this.

I was unable to create a new site without an application password. Is this still possible?

I don't think it is, but you can always go to wp-admin and revoke all application passwords for your user.

Screenshot 2026-02-17 at 4 23 22 PM

@dcalhoun
Copy link
Copy Markdown
Member

I was unable to create a new site without an application password. Is this still possible?

I don't think it is, but you can always go to wp-admin and revoke all application passwords for your user.

I don't believe that would result in the client app having the necessary username and password for authenticating via cookies, though. At that point, it would have an invalid app password and then prompt the user to create a new app password. IINM, testing cookie authentication is the target of that particular testing instructions.

@dcalhoun
Copy link
Copy Markdown
Member

I suppose the required approach may be adding a site with a previous app release, then upgrading to this prototype build.

@adalpari
Copy link
Copy Markdown
Contributor Author

I suppose the required approach may be adding a site with a previous app release, then upgrading to this prototype build.

This ^^^

We removed the non-AP authentication. So, the only way is to add a user with an old version of the app. Maybe we should support it in order to debug cases like this one.

Anyway, the testing step is to prevent a regression but no, we cannot reproduce it with the current version

adalpari and others added 6 commits February 18, 2026 15:40
ReactNativeStore used cookie-nonce auth for all non-WPCom sites,
which fails for sites with application passwords since those
credentials can't log in via wp-login.php. This adds Basic auth
support using the site's application password credentials directly,
bypassing the nonce flow for those sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add LongMethod and ReturnCount suppress annotations to
executeWPAPIRequest and remove unused eq and Unknown imports
from tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add defensive null checks for application password credentials
to prevent potential NPE from race conditions. Expand inline
documentation explaining the nonce auth bypass rationale.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing any() matcher for the headers parameter added to
syncGetRequest and syncPostRequest in WPAPIGsonRequestBuilder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari adalpari force-pushed the adalpari/fix-media-401-auth branch from 553c131 to 9da8188 Compare February 18, 2026 14:40
@adalpari
Copy link
Copy Markdown
Contributor Author

@adalpari Claude is flagging the removal of the nonce unknown test as an issue. I added it back locally and it passes. What was the reason for its removal?

Good catch! I think it was removed in one of the latest cleanings, so I missed it.
I've re-add it: 9da8188

cc @nbradbury

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Member

@dcalhoun dcalhoun left a comment

Choose a reason for hiding this comment

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

I tested a self-hosted site with cookie authentication after adding the site with a prototype build from #22454.

@adalpari adalpari merged commit 64697b7 into trunk Feb 18, 2026
24 checks passed
@adalpari adalpari deleted the adalpari/fix-media-401-auth branch February 18, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants