@@ -202,17 +202,23 @@ public function token_request( WP_User $user ): void {
202202 $ code_verifier = $ this ->code_verifier ->generate ( $ user ->user_email );
203203 $ this ->code_verifier_repository ->store_code_verifier ( $ user ->ID , $ code_verifier ->get_code (), $ code_verifier ->get_created_at () );
204204
205+ $ callback_url = $ this ->urls ->get_callback_url ();
206+ $ refresh_callback_url = $ this ->urls ->get_refresh_callback_url ();
207+
205208 $ request_body = [
206209 'service ' => 'openai ' ,
207210 'code_challenge ' => \hash ( 'sha256 ' , $ code_verifier ->get_code () ),
208211 'license_site_url ' => WPSEO_Utils::get_home_url (),
209212 'user_id ' => (string ) $ user ->ID ,
210- 'callback_url ' => $ this -> urls -> get_callback_url () ,
211- 'refresh_callback_url ' => $ this -> urls -> get_refresh_callback_url () ,
213+ 'callback_url ' => $ callback_url ,
214+ 'refresh_callback_url ' => $ refresh_callback_url ,
212215 ];
213216
214217 $ this ->request_handler ->handle ( new Request ( '/token/request ' , $ request_body ) );
215218
219+ // Store a per-user hash of the callback URL to detect future site URL changes.
220+ $ this ->user_helper ->update_meta ( $ user ->ID , '_yoast_wpseo_ai_generator_callback_url_hash ' , \md5 ( $ callback_url ) );
221+
216222 // The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
217223 \wp_cache_delete ( $ user ->ID , 'user_meta ' );
218224 }
@@ -306,6 +312,12 @@ public function has_token_expired( string $jwt ): bool {
306312 * @throws RuntimeException Unable to retrieve the access or refresh token.
307313 */
308314 public function get_or_request_access_token ( WP_User $ user ): string {
315+ // If the site URL has changed since callback URLs were registered, delete stale tokens.
316+ if ( $ this ->have_callback_urls_changed ( $ user ) ) {
317+ $ this ->user_helper ->delete_meta ( $ user ->ID , '_yoast_wpseo_ai_generator_access_jwt ' );
318+ $ this ->user_helper ->delete_meta ( $ user ->ID , '_yoast_wpseo_ai_generator_refresh_jwt ' );
319+ }
320+
309321 $ access_jwt = $ this ->user_helper ->get_meta ( $ user ->ID , '_yoast_wpseo_ai_generator_access_jwt ' , true );
310322 if ( ! \is_string ( $ access_jwt ) || $ access_jwt === '' ) {
311323 $ this ->token_request ( $ user );
@@ -330,4 +342,30 @@ public function get_or_request_access_token( WP_User $user ): string {
330342 }
331343
332344 // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
345+
346+ /**
347+ * Checks whether the callback URLs have changed since the last token request.
348+ *
349+ * Detects site URL changes (e.g., migrating from a staging URL to a production domain)
350+ * that would leave stale callback URLs registered with the Yoast AI service.
351+ * Uses a per-user hash so each user independently detects the change and re-registers.
352+ * The hash is immune to wp search-replace operations.
353+ *
354+ * When no hash is stored (first run after upgrade), returns true to force a fresh
355+ * token_request(). This ensures existing sites with stale callback URLs self-heal
356+ * without manual intervention.
357+ *
358+ * @param WP_User $user The current user.
359+ *
360+ * @return bool Whether the callback URLs may have changed.
361+ */
362+ private function have_callback_urls_changed ( WP_User $ user ): bool {
363+ $ registered_hash = $ this ->user_helper ->get_meta ( $ user ->ID , '_yoast_wpseo_ai_generator_callback_url_hash ' , true );
364+
365+ if ( ! \is_string ( $ registered_hash ) || $ registered_hash === '' ) {
366+ return true ;
367+ }
368+
369+ return $ registered_hash !== \md5 ( $ this ->urls ->get_callback_url () );
370+ }
333371}
0 commit comments