| @@ -8,8 +8,50 @@ | ||
| 8 | 8 | defined('ABSPATH') || exit; |
| 9 | 9 | |
| 10 | 10 | class OAuthManager |
| 11 | 11 | { |
| 12 | + /** Prefix for the per-user transient holding a pending OAuth authorization. */ | |
| 13 | + const OAUTH_STATE_TRANSIENT = 'sync_basalam_oauth_state_'; | |
| 14 | + | |
| 15 | + /** Lifetime of a pending OAuth authorization — the SSO round-trip window. */ | |
| 16 | + const OAUTH_STATE_TTL = 600; // 10 * MINUTE_IN_SECONDS | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * Remember that the current admin has just started an OAuth authorization. | |
| 20 | + * | |
| 21 | + * This is called only from the nonce-protected initiation flow, so the | |
| 22 | + * marker it stores cannot be planted by a forged cross-site request. The | |
| 23 | + * callback later requires (and consumes) this marker, which is what turns | |
| 24 | + * the token-saving callback from "always forgeable" into "only valid for a | |
| 25 | + * flow this admin actually started". | |
| 26 | + */ | |
| 27 | + public static function issueOauthState() | |
| 28 | + { | |
| 29 | + $state = wp_generate_password(64, false); | |
| 30 | + set_transient(self::OAUTH_STATE_TRANSIENT . get_current_user_id(), $state, self::OAUTH_STATE_TTL); | |
| 31 | + | |
| 32 | + return $state; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * Validate and consume the pending OAuth authorization for the current user. | |
| 37 | + * | |
| 38 | + * Single use: the marker is deleted whether or not it was present, so a | |
| 39 | + * replayed or forged callback cannot reuse it. | |
| 40 | + */ | |
| 41 | + private static function verifyOauthState() | |
| 42 | + { | |
| 43 | + $key = self::OAUTH_STATE_TRANSIENT . get_current_user_id(); | |
| 44 | + $expected = get_transient($key); | |
| 45 | + delete_transient($key); | |
| 46 | + | |
| 47 | + // The token exchange is routed back through the Hamsalam proxy, which | |
| 48 | + // consumes the SSO "state" (the site URL) and does not forward a secret | |
| 49 | + // we control. The single-use marker set during the authenticated | |
| 50 | + // initiation is therefore the value that authorises the write. | |
| 51 | + return ! empty($expected); | |
| 52 | + } | |
| 53 | + | |
| 12 | 54 | public function getOauthData() |
| 13 | 55 | { |
| 14 | 56 | $oauthDataUrl = apply_filters('sync_basalam_oauth_data_url', Endpoints::HAMSALAM_OAUTH_DATA); |
| 15 | 57 | $defaultClientId = apply_filters('sync_basalam_oauth_default_client_id', 779); |
| @@ -32,8 +74,20 @@ | ||
| 32 | 74 | } |
| 33 | 75 | |
| 34 | 76 | public static function saveOauthData() |
| 35 | 77 | { |
| 78 | + // CSRF protection: this callback performs a state-changing write from a | |
| 79 | + // plain GET, so it must be tied to an OAuth flow the current admin | |
| 80 | + // actually initiated. Without this an attacker could lure a logged-in | |
| 81 | + // admin to the callback URL and overwrite the stored Basalam credentials. | |
| 82 | + if (! current_user_can('manage_options') || ! self::verifyOauthState()) { | |
| 83 | + wp_die( | |
| 84 | + esc_html__('درخواست نامعتبر است.', 'sync-basalam'), | |
| 85 | + esc_html__('خطای امنیتی', 'sync-basalam'), | |
| 86 | + ['response' => 403] | |
| 87 | + ); | |
| 88 | + } | |
| 89 | + | |
| 36 | 90 | $isVendor = isset($_GET['is_vendor']) ? sanitize_text_field(wp_unslash($_GET['is_vendor'])) : true; |
| 37 | 91 | $vendorId = isset($_GET['vendor_id']) ? sanitize_text_field(intval($_GET['vendor_id'])) : null; |
| 38 | 92 | $hamsalamToken = isset($_GET['hamsalam_token']) ? sanitize_text_field(wp_unslash($_GET['hamsalam_token'])) : null; |
| 39 | 93 | $hamsalamBusinessId = isset($_GET['hamsalam_business_id']) ? sanitize_text_field(wp_unslash($_GET['hamsalam_business_id'])) : null; |
| @@ -72,9 +126,9 @@ | ||
| 72 | 126 | { |
| 73 | 127 | $oauthData = $this->getOauthData(); |
| 74 | 128 | $siteUrl = get_site_url(); |
| 75 | 129 | |
| 76 | - $scopes = apply_filters('sync_basalam_oauth_scopes', "vendor.product.write vendor.parcel.write customer.profile.read vendor.profile.read vendor.parcel.read vendor.profile.write customer.chat.read customer.chat.write"); | |
| 130 | + $scopes = apply_filters('sync_basalam_oauth_scopes', "vendor.product.write vendor.parcel.write customer.profile.read vendor.profile.read vendor.parcel.read vendor.profile.write customer.chat.read customer.chat.write customer.identity.read"); | |
| 77 | 131 | |
| 78 | 132 | return [ |
| 79 | 133 | 'redirect_uri' => $oauthData['redirect_uri'], |
| 80 | 134 | 'url_req_token' => Endpoints::oauthLoginUrl( |