Skip to content

Allow headless application-password creation on Atomic sites#22885

Merged
jkmassel merged 2 commits into
trunkfrom
jkmassel/issue-22884
May 26, 2026
Merged

Allow headless application-password creation on Atomic sites#22885
jkmassel merged 2 commits into
trunkfrom
jkmassel/issue-22884

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented May 21, 2026

Description

Fixes #22884.

Stacked PRs. This is the middle of a three-PR stack:

Problem

ApplicationPasswordsManager.getApplicationCredentials returned NotSupported for any site.isWPCom site. The guard was correct for Simple sites but blocked Atomic sites, which are also isWPCom-flagged and do support REST application-password creation. Users on Atomic saw the "Authenticate using Application Password" card on My Site and had to authorize through a Chrome Custom Tab even though the app could mint the credential on their behalf.

What changed

  1. Relax the FluxC guard (ApplicationPasswordsManager.kt:45): site.isWPComsite.isWPComSimpleSite. Atomic falls through to the existing origin == ORIGIN_WPCOM_REST branches and the Jetpack-tunnel JetpackApplicationPasswordsRestClient.

  2. Auto-mint before showing the card (ApplicationPasswordViewModelSlice + new SiteStore.createApplicationPassword): before rendering the "Authenticate" card, attempt headless creation via the FluxC manager. On success, persist credentials onto the SiteModel (so siteHasBadCredentials flips false) and skip the card. On failure, fall back to the existing discovery + Custom Tab card.

  3. Fix the launch crash (ApplicationPasswordsClientIdModule + SiteStore guard): the WordPress / Jetpack apps had never bound @ApplicationPasswordsClientId in their Dagger graphs, so any call into ApplicationPasswordsStore threw NoSuchElementException. This was latent — no production caller hit the path on these apps — until step 2 above started routing My Site through it and crashed the app on launch. Provide the binding with the same device-interpolated name the Custom Tab flow already uses ("Jetpack Android App on <device>" / "WordPress Android App on …"); also short-circuit SiteStore.createApplicationPassword to NotSupported when the configuration is disabled, so we degrade gracefully if the binding goes missing again.

  4. Validate-then-mint pipeline (new ApplicationPasswordValidator, slice restructure): collapse the previous isUsingSelfHostedRestApi → validate vs else → discovery+card split into a single uniform validate → mint → card flow that works for every site type.

    The validator hits users().retrieveMeWithViewContext() via WpApiClientProvider.getApplicationPasswordClient(site) (introduced in Route the application-passwords list screen through Basic auth #22894) — always Basic auth against the direct host. The previous validation went through getWpApiClient, which for Atomic returns the WPCom bearer client and never actually exercised the application password, so revoked passwords on Atomic were undetectable. On Invalid, the slice clears stored creds via a new SiteStore.deleteStoredApplicationPasswordCredentials and falls through to mint; on NetworkUnavailable, the card stays hidden.

    The mint step calls SiteStore.createApplicationPassword from item 2. The card step shows the reauth banner if we started with creds, the "authenticate" card otherwise — either way discovery still populates the Custom Tab URL.

    The XML-RPC-disabled card path is now gated on !isUsingWpComRestApi so it only fires for true self-hosted sites — Atomic and Jetpack-WPCom-REST sites talk REST end-to-end.

Site type coverage

Site type Today (trunk) After this PR
Atomic, no app pwd Card → Chrome Custom Tab Validate skipped → FluxC mint succeeds → no card
Atomic, valid app pwd No card No card (validation succeeds)
Jetpack-WPCom-REST, no app pwd Card → Chrome Custom Tab FluxC mint succeeds → no card
Simple WPCom No card (discovery empty) No card (FluxC mint NotSupported short-circuits)
Self-hosted with valid creds Validate path Same flow; validation now actually uses Basic auth
Self-hosted without creds Card → Chrome Custom Tab Same — mint Failure → discovery → card

The "revoke and recover" Atomic case is covered by #22893.

On-device verification

Confirmed against jeremyseriousbusinesstesting.wpcomstaging.com (Atomic):

  • Cold launch with no stored creds → FluxC mint succeeds → card hidden. ✅
  • Subsequent loads → validate succeeds → no card, no re-mint. ✅

Tests

Adds two FluxC manager cases (simple-WPCom NotSupported; Atomic Jetpack-tunnel mint) in ApplicationPasswordManagerTests, six slice cases in ApplicationPasswordViewModelSliceTest covering the new validate → mint → card flow, and a small standalone OnApplicationPasswordCreateErrorTest for the FluxC error class.

Testing instructions

The scenarios below are the ones this PR alone covers — they exercise the auto-mint and the happy-path validation. To test what happens when a server-side credential is revoked, when validation hits a transient error, or when the device is offline mid-validate, build from #22893 (PR3 is stacked on this PR and adds the validator hardening that makes those flows recoverable).

Atomic happy path — first foreground

  1. Sign in to Jetpack debug with a WP.com account that owns an Atomic site that does not have a pre-existing application password for this device.
  2. Open the Atomic site on My Site.
  • No "Authenticate using Application Password" card.
  • Logcat (adb logcat -s WordPress-MAIN | grep A_P:) shows Headless mint succeeded.
  • wp-admin → Users → Profile → Application Passwords shows a new entry named Jetpack Android App on <your device>.

Atomic — subsequent loads with valid creds

  1. After the happy path above, force-stop the app and reopen it.
  2. Open the same Atomic site on My Site.
  • No card.
  • Logcat shows A_P: Validation Success returned user id=... followed by Hiding card for ... - authenticated. No re-mint attempt.

Simple WPCom regression

  1. Switch to a Simple WPCom site on My Site.
  • No card. Logcat shows the mint returning NotSupported and discovery returning empty.

Self-hosted regression (true self-hosted, not Atomic)

  1. Self-hosted site with an existing app password:
  • Card hidden; validation uses Basic auth against the direct host.
  1. Self-hosted site without an app password:
  • Card appears; tapping still launches the Chrome Custom Tab authorize flow.

Out of scope here (covered by #22893)

These scenarios will not behave correctly against PR2 alone — the validator's classification still has the issues found during the initial on-device testing of this PR:

If you want to exercise the full revoke-and-recover flow on-device, build from the jkmassel/issue-22884-validator-hardening branch (or check out #22893's head).

Related

@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented May 21, 2026

2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ This PR is assigned to the milestone 26.8. The due date for this milestone has already passed.
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented May 21, 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
Build TypeDebug
Versionpr22885-5bfe308
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit5bfe308
Installation URL6cbtrpckv8e90
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented May 21, 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
Build TypeDebug
Versionpr22885-5bfe308
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit5bfe308
Installation URL21933iroi2pd8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 49.71751% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.34%. Comparing base (941ac5b) to head (5bfe308).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
...ava/org/wordpress/android/fluxc/store/SiteStore.kt 23.91% 35 Missing ⚠️
...pplicationpassword/ApplicationPasswordValidator.kt 3.57% 27 Missing ⚠️
...ationpassword/ApplicationPasswordViewModelSlice.kt 75.00% 7 Missing and 6 partials ⚠️
...word/ApplicationPasswordAutoAuthDialogViewModel.kt 25.00% 4 Missing and 2 partials ⚠️
...i/accounts/login/ApplicationPasswordLoginHelper.kt 85.71% 0 Missing and 4 partials ⚠️
...npassword/LoginSiteApplicationPasswordViewModel.kt 33.33% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22885      +/-   ##
==========================================
+ Coverage   37.32%   37.34%   +0.01%     
==========================================
  Files        2319     2320       +1     
  Lines      124585   124653      +68     
  Branches    16928    16941      +13     
==========================================
+ Hits        46506    46549      +43     
- Misses      74318    74339      +21     
- Partials     3761     3765       +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.

@wpmobilebot
Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@jkmassel jkmassel force-pushed the jkmassel/issue-22884 branch 2 times, most recently from 1888111 to 912dd2e Compare May 25, 2026 17:44
@jkmassel jkmassel force-pushed the jkmassel/issue-22884 branch from 912dd2e to a123523 Compare May 25, 2026 17:56
@jkmassel jkmassel changed the base branch from trunk to jkmassel/issue-22884-list-screen-direct-host May 25, 2026 17:58
@jkmassel jkmassel added this to the 26.8 milestone May 25, 2026
@jkmassel jkmassel requested a review from adalpari May 25, 2026 18:51
@jkmassel jkmassel force-pushed the jkmassel/issue-22884 branch from dc9ff28 to cc93500 Compare May 25, 2026 19:10
@jkmassel jkmassel marked this pull request as ready for review May 25, 2026 19:12
Base automatically changed from jkmassel/issue-22884-list-screen-direct-host to trunk May 26, 2026 15:39
@jkmassel jkmassel force-pushed the jkmassel/issue-22884 branch from cc93500 to 80b1ebf Compare May 26, 2026 15:41
Copy link
Copy Markdown
Contributor

@oguzkocer oguzkocer left a comment

Choose a reason for hiding this comment

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

I left a few comments, but none of them are blockers.

I've tested what I could, and almost everything worked as expected. I found one issue, where the Invalid Application Password dialogue couldn't be dismissed.

invalid_password.webm

I think this ^ issue was introduced in this PR. Here is how I produced it:

  1. Add a self-hosted site with application password
  2. Revoke the application password online
  3. Pull the My Site screen to refresh

Note that I couldn't test 2 things:

  1. For atomic site, I couldn't check the token server side, but the flow worked as expected. (I can't login to the site on the web for some reason)
  2. I couldn't test tapping the banner because I couldn't dismiss the Invalid Application Password dialogue.

private var processedAppPasswordData: String? = null

sealed class DiscoveryResult {
data class Authorized(val authorizationUrl: String) : DiscoveryResult()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had to do a double take while reviewing the diff in WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModel.kt, specifically this part:

when (val result = applicationPasswordLoginHelper.getAuthorizationUrlComplete(siteUrl)) {
    is ApplicationPasswordLoginHelper.DiscoveryResult.Authorized ->
        _navigationEvent.emit(NavigationEvent.FallbackToManualLogin(result.authorizationUrl))

Authorized > Fallback confused me a little bit. That Authorized just means the discovery succeeded, so the implementation looks correct to me. However, if we change the name to Success or Discovered, it might reduce some of the friction at call sites. Wdyt?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think this could be improved in the library for sure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I actually meant maybe we wanted to rename DiscoveryResult::Authorized sealed class variant, but either way it was a small issue.

}
}

enum class Outcome { Valid, Invalid, NetworkUnavailable }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this is being improved on in a subsequent PR, so I am just noting my thoughts. There might be a few cases where we return Outcome.Invalid when it's really Outcome.Unreliable (for lack of a better term) and I wonder if it'd be better if we were strict about when something is Invalid and go with Unreliable/NeedsRetry type of outcome for others.

Maybe NetworkUnavailable covers everything we need, and expanding on that in the subsequent PR will get us exactly where we want to be. I used a different term above, because I wasn't sure if network availability is the only case where we'd want to retry vs invalidate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will be addressed by #22893

}

// Validate credentials by making a simple API call
scope.launch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is an existing issue, but we are not handling cancellations so it results in double logs like so:

A_P: Headless mint succeeded for https://ouzgutenbergkitatomicsite.wpcomstaging.com
A_P: Hiding card for https://ouzgutenbergkitatomicsite.wpcomstaging.com - authenticated
A_P: Headless mint succeeded for https://ouzgutenbergkitatomicsite.wpcomstaging.com
A_P: Hiding card for https://ouzgutenbergkitatomicsite.wpcomstaging.com - authenticated

I think this might mean that we are creating multiple application passwords on the server side. For some reason I can't login to the site with my WordPress.com account to confirm.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice catch – addressed by 06231eb

jkmassel added a commit that referenced this pull request May 26, 2026
ApplicationPasswordViewModelSlice.buildCard fires from MySiteViewModel's
onResume / refresh / onSitePicked. Without job tracking, two close-together
calls race: both pass ApplicationPasswordsManager's `existingPassword ==
null` check and issue separate server-side mints. The 409 conflict handler
then deletes-and-recreates the winner's password, so the losing racer
destroys the credential it just helped create.

Pre-PR the slice raced on validate-only, which had no server-side side
effect. The new mint step in #22885 makes the race visible and harmful.

Fix: track the in-flight Job and drop subsequent calls while it's active.
The running call posts its result regardless, so the user-visible state
still updates.

Test gates `createApplicationPassword` on a `CompletableDeferred`, fires
buildCard twice, and verifies a single mint invocation reaches the store.
@jkmassel
Copy link
Copy Markdown
Contributor Author

The dialog issue should also be addressed by #22893

@jkmassel jkmassel requested a review from oguzkocer May 26, 2026 21:49
jkmassel added 2 commits May 26, 2026 16:13
Fixes #22884.

`ApplicationPasswordsManager.getApplicationCredentials` returned `NotSupported` for any
`site.isWPCom` site. The guard was correct for Simple sites but blocked Atomic sites, which
are also `isWPCom`-flagged and do support REST application-password creation. Users on Atomic
saw the "Authenticate using Application Password" card on My Site and had to authorize through
a Chrome Custom Tab even though the app could mint the credential on their behalf.

Relax the FluxC guard from `site.isWPCom` to `site.isWPComSimpleSite` and add a uniform
validate-then-mint pipeline to `ApplicationPasswordViewModelSlice`: validate stored creds via
wordpress-rs Basic auth against the direct host (new `ApplicationPasswordValidator`, using
`WpApiClientProvider.getApplicationPasswordClient` from #22894); on Invalid, clear them via a
new `SiteStore.deleteStoredApplicationPasswordCredentials` and fall through to mint via a new
`SiteStore.createApplicationPassword` (FluxC Jetpack tunnel); on mint failure, the existing
discovery + card path takes over. The XML-RPC-disabled card path is now gated on
`!isUsingWpComRestApi` so it only fires for true self-hosted sites.

Also provides the missing `@ApplicationPasswordsClientId` Dagger binding — without it any call
into `ApplicationPasswordsStore` threw `NoSuchElementException`. The path was latent on these
apps until the auto-mint above started routing My Site through it.
ApplicationPasswordViewModelSlice.buildCard fires from MySiteViewModel's
onResume / refresh / onSitePicked. Without job tracking, two close-together
calls race: both pass ApplicationPasswordsManager's `existingPassword ==
null` check and issue separate server-side mints. The 409 conflict handler
then deletes-and-recreates the winner's password, so the losing racer
destroys the credential it just helped create.

Pre-PR the slice raced on validate-only, which had no server-side side
effect. The new mint step in #22885 makes the race visible and harmful.

Fix: track the in-flight Job and drop subsequent calls while it's active.
The running call posts its result regardless, so the user-visible state
still updates.

Test gates `createApplicationPassword` on a `CompletableDeferred`, fires
buildCard twice, and verifies a single mint invocation reaches the store.
@jkmassel jkmassel force-pushed the jkmassel/issue-22884 branch from 06231eb to 5bfe308 Compare May 26, 2026 22:13
@jkmassel jkmassel enabled auto-merge (squash) May 26, 2026 22:14
Copy link
Copy Markdown
Contributor

@oguzkocer oguzkocer left a comment

Choose a reason for hiding this comment

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

The fix for the double token creation looks good to me. I've tested it by:

  1. Revoking the application password online (I was previously using the wrong WordPress.com account to login 🤦, so this time I was able to test it properly)
  2. Pulled to refresh
  3. Verified that only one token was created from web
  4. Verified through the application logs

Looks good to me. :shipit:

@jkmassel jkmassel merged commit 96fc478 into trunk May 26, 2026
20 of 22 checks passed
@jkmassel jkmassel deleted the jkmassel/issue-22884 branch May 26, 2026 22:31
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.

Allow headless application-password creation on Atomic sites

4 participants