From 4bea87ac645d0c14600013907e262096e345358b Mon Sep 17 00:00:00 2001 From: Tanner Young Date: Thu, 2 Apr 2026 13:46:35 -0700 Subject: [PATCH 1/4] feat: add OAuth flow nonce generation and verification helpers Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/patreon_login.php | 61 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/classes/patreon_login.php b/classes/patreon_login.php index 286bdd7..094b305 100644 --- a/classes/patreon_login.php +++ b/classes/patreon_login.php @@ -23,6 +23,67 @@ public static function clear_user_token_data($user_id) delete_user_meta($user_id, 'patreon_token_minted'); } + /** + * Generate an OAuth flow nonce for the current user and store it as a transient. + * Returns an array with 'nonce' and 'user_id' to embed in the OAuth state parameter. + * If no user is logged in, returns an empty array. + */ + public static function generate_oauth_flow_nonce() + { + if (!is_user_logged_in()) { + return []; + } + + $user_id = get_current_user_id(); + $nonce = wp_generate_password(32, false); + $transient_key = 'patreon_oauth_nonce_' . $user_id; + + // Store nonce with 10-minute TTL + set_transient($transient_key, $nonce, 10 * MINUTE_IN_SECONDS); + + return [ + 'oauth_nonce' => $nonce, + 'oauth_user_id' => $user_id, + ]; + } + + /** + * Verify that the OAuth flow nonce in the state parameter matches the logged-in user. + * Returns true if the nonce is valid and belongs to the current user, false otherwise. + */ + public static function verify_oauth_flow_nonce($state) + { + if (!is_user_logged_in()) { + return false; + } + + if (!isset($state['oauth_nonce']) || !isset($state['oauth_user_id'])) { + return false; + } + + $user_id = get_current_user_id(); + + // The state must claim to belong to the currently logged-in user + if ((int) $state['oauth_user_id'] !== $user_id) { + return false; + } + + $transient_key = 'patreon_oauth_nonce_' . $user_id; + $stored_nonce = get_transient($transient_key); + + if (false === $stored_nonce) { + return false; + } + + // Constant-time comparison + $valid = hash_equals($stored_nonce, $state['oauth_nonce']); + + // Delete the transient after use (one-time use nonce) + delete_transient($transient_key); + + return $valid; + } + public static function updateExistingUser($user_id, $user_response, $tokens) { /* update user meta data with patreon data */ From 2943befd2d26b7f8062897a1fa53db249d21185d Mon Sep 17 00:00:00 2001 From: Tanner Young Date: Thu, 2 Apr 2026 13:47:48 -0700 Subject: [PATCH 2/4] feat: embed OAuth flow nonce in state parameter at all flow initiation points Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/patreon_routing.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/classes/patreon_routing.php b/classes/patreon_routing.php index ba91536..5f2080c 100644 --- a/classes/patreon_routing.php +++ b/classes/patreon_routing.php @@ -80,6 +80,9 @@ public function parse_request(&$wp) 'final_redirect_uri' => $final_redirect, ]; + // Embed OAuth flow nonce so the callback can verify which user initiated this flow + $state = array_merge($state, Patreon_Login::generate_oauth_flow_nonce()); + // Below filter vars and the following filter allows plugin devs to acquire/filter info about Patron/user + content before going to Patreon flow $filter_args = [ @@ -128,6 +131,9 @@ public function parse_request(&$wp) $state['final_redirect_uri'] = $redirect; $send_pledge_level = $patreon_level * 100; + // Embed OAuth flow nonce so the callback can verify which user initiated this flow + $state = array_merge($state, Patreon_Login::generate_oauth_flow_nonce()); + $flow_link = Patreon_Frontend::MakeUniversalFlowLink($send_pledge_level, $state, $client_id, $post, ['link_interface_item' => $link_interface_item]); wp_redirect($flow_link); @@ -212,6 +218,9 @@ public function parse_request(&$wp) $state['final_redirect_uri'] = $final_redirect; + // Embed OAuth flow nonce so the callback can verify which user initiated this flow + $state = array_merge($state, Patreon_Login::generate_oauth_flow_nonce()); + $send_pledge_level = $patreon_level * 100; // Below filter vars and the following filter allows plugin devs to acquire/filter info about Patron/user + content before going to Patreon flow From 9b3d61b3b83198450106bf84a658a25665b0d7da Mon Sep 17 00:00:00 2001 From: Tanner Young Date: Thu, 2 Apr 2026 13:48:20 -0700 Subject: [PATCH 3/4] feat: verify OAuth flow nonce before linking Patreon identity to logged-in user Prevents same-browser session collision where an OAuth callback overwrites the logged-in admin's Patreon identity with a different patron's data. Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/patreon_login.php | 7 ++++--- classes/patreon_routing.php | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/classes/patreon_login.php b/classes/patreon_login.php index 094b305..f476162 100644 --- a/classes/patreon_login.php +++ b/classes/patreon_login.php @@ -166,7 +166,7 @@ public static function checkTokenExpiration($user_id = false) } } - public static function createOrLogInUserFromPatreon($user_response, $tokens, $redirect = false) + public static function createOrLogInUserFromPatreon($user_response, $tokens, $redirect = false, $state = []) { global $wpdb; @@ -176,8 +176,9 @@ public static function createOrLogInUserFromPatreon($user_response, $tokens, $re // Check if user is logged in to wp: - // Logged in user. We just link the user up and be done. - if (is_user_logged_in()) { + // Logged in user. Only link if the OAuth flow nonce confirms this user initiated the flow. + // If verification fails, fall through to the not-logged-in path (lookup/create). + if (is_user_logged_in() && self::verify_oauth_flow_nonce($state)) { $user = wp_get_current_user(); self::updateExistingUser($user->ID, $user_response, $tokens); diff --git a/classes/patreon_routing.php b/classes/patreon_routing.php index 5f2080c..d268686 100644 --- a/classes/patreon_routing.php +++ b/classes/patreon_routing.php @@ -595,7 +595,8 @@ private function handle_authorization_flow($wp) if (apply_filters('ptrn/force_strict_oauth', get_option('patreon-enable-strict-oauth', false))) { $user = Patreon_Login::updateLoggedInUserForStrictoAuth($user_response, $tokens, $redirect); } else { - $user = Patreon_Login::createOrLogInUserFromPatreon($user_response, $tokens, $redirect); + $state_for_login = isset($state) ? $state : []; + $user = Patreon_Login::createOrLogInUserFromPatreon($user_response, $tokens, $redirect, $state_for_login); } // shouldn't get here From 8de180b0f0b9002d441a0c3822e57509e7d20427 Mon Sep 17 00:00:00 2001 From: Tanner Young Date: Thu, 2 Apr 2026 13:48:50 -0700 Subject: [PATCH 4/4] fix: use Patreon-specific meta keys for patron names Rename user_firstname/user_lastname to patreon_user_firstname/patreon_user_lastname so that syncing Patreon profile data does not silently overwrite the WordPress user's display name and profile fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/patreon_user_profiles.php | 8 ++++---- classes/patreon_wordpress.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/classes/patreon_user_profiles.php b/classes/patreon_user_profiles.php index f073cb4..1fc8f96 100644 --- a/classes/patreon_user_profiles.php +++ b/classes/patreon_user_profiles.php @@ -39,15 +39,15 @@ public function patreon_user_profile_fields($user) - + -
+
- + -
+
diff --git a/classes/patreon_wordpress.php b/classes/patreon_wordpress.php index 78e1468..07003c9 100644 --- a/classes/patreon_wordpress.php +++ b/classes/patreon_wordpress.php @@ -319,8 +319,8 @@ public static function updatePatreonUser() } update_user_meta($user->ID, 'patreon_created', $patreon_created); - update_user_meta($user->ID, 'user_firstname', $user_response['data']['attributes']['first_name']); - update_user_meta($user->ID, 'user_lastname', $user_response['data']['attributes']['last_name']); + update_user_meta($user->ID, 'patreon_user_firstname', $user_response['data']['attributes']['first_name']); + update_user_meta($user->ID, 'patreon_user_lastname', $user_response['data']['attributes']['last_name']); } }