Paid Content block: refresh JWT on-demand instead of denying on stale end_date#48008
Paid Content block: refresh JWT on-demand instead of denying on stale end_date#48008allilevine wants to merge 2 commits intotrunkfrom
Conversation
When a paid subscriber's subscription renews, the JWT cookie still contains the old end_date snapshot from before the renewal. validate_subscriptions() denies access because the stale date is now in the past, even though the subscription is active in billing. This change adds a refresh-before-deny path to visitor_can_view_content(): when the token's subscriptions do not satisfy the required plans, the service POSTs the existing token to a new WordPress.com refresh endpoint that re-queries billing and returns a fresh token. On success, the cookie is replaced with the fresh token and access is re-evaluated. Response handling: - 200 with jwt_token: replace cookie, re-run validation - 400 (iat expired) / 401 (bad signature): clear cookie, deny - 5xx / network error / timeout: leave cookie, deny (transient) Note: the refresh endpoint (memberships/jwt/refresh) must be deployed on WordPress.com before this change ships. This PR should be held as a draft until the endpoint is live and serving 200s in production. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Jetpack plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
Code Coverage SummaryCannot generate coverage summary while tests are failing. 🤐 Please fix the tests, or re-run the Code coverage job if it was something being flaky. |
The WordPress.com refresh endpoint (wpcom#210997) returns the fresh
token as { token: "<jwt>" } in the response body. Update fetch_refreshed_token()
and the test mocks to read/write "token" instead of "jwt_token".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes NL-546
Proposed changes
Abstract_Token_Subscription_Service::visitor_can_view_content(): when the token's subscriptions do not satisfy the required plans, POST the existing token to a new WordPress.com refresh endpoint (subscribe.wordpress.com/memberships/jwt/refresh) that re-queries billing and returns a fresh token reflecting current subscription state.refresh_token_payload()andfetch_refreshed_token()on the abstract service class. Declare the previously implicitget_site_id()contract as abstract since the refresh call needs it.Response handling
Other information
memberships/jwt/refresh) is live in production and serving 200s. If merged first, Jetpack will hit a 404 on every stale-token request and fall back to the current (broken) behavior. Leaving this PR as a draft until the server-side work is verified.Root cause: When a paid subscriber's subscription renews, Billing system records the new
end_date, but the JWT cookie still contains the old snapshot.validate_subscriptions()checksend_date > time()against the stale cookie value and denies access. The subscriber sees a "subscribe again" prompt but is told they're already subscribed.Scale: ~10,953 subscribers renewing in the next 30 days across 1,421 sites are affected. Reports from at least 7 different sites over 5 years.
Why this over the grace period approach: A 1-day grace period (PR #47773) would cover the renewal window but leaves the underlying architecture unchanged — the JWT remains a static snapshot that never learns about billing events. This approach self-heals on any stale-end_date condition, not just renewal windows, and sets up the path to eventually drop
end_datefrom the token entirely.Related product discussion/links
Does this pull request change what data or activity we track or use?
No new data. Adds an outbound HTTP POST to
subscribe.wordpress.com/memberships/jwt/refreshcarrying the existing JWT cookie and the site ID — both already known to WordPress.com.Testing instructions
Prerequisite
The
memberships/jwt/refreshendpoint must be live on WordPress.com. Confirm by checking that a POST with a valid signed JWT returns 200 with a freshjwt_tokenin the response body.Manual test — successful refresh
end_date) and append it as?token=to the URL:?token=<output>.end_date, called the refresh endpoint, received a fresh token, and granted access. Check the cookie — it should contain a new JWT.Manual test — genuine cancellation
PHPUnit tests
New tests in
Jetpack_Premium_Content_Test.php:test_refresh_before_deny_grants_access_on_successful_refreshtest_refresh_before_deny_denies_when_fresh_token_has_no_subscriptiontest_refresh_before_deny_denies_on_transient_failuretest_refresh_before_deny_clears_cookie_on_unauthorizedtest_refresh_before_deny_treats_wp_error_as_transienttest_refresh_not_called_when_subscription_is_activeAll tests mock the refresh endpoint via the
pre_http_requestfilter to exercise each response branch without making real HTTP calls.