diff --git a/classes/patreon_login.php b/classes/patreon_login.php index 286bdd7..f476162 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 */ @@ -105,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; @@ -115,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 ba91536..d268686 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 @@ -586,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 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)