PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.19
ووسلام – همگام سازی ووکامرس و باسلام v1.10.19
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
← All changes | includes/Admin/Settings/OAuthManager.php +15 -117 1.10.201.10.19 View file →
@@ -8,10 +8,10 @@
8 8 defined('ABSPATH') || exit;
9 9
10 10 class OAuthManager
11 11 {
12 - /** Signed, browser-bound proof that an administrator started OAuth. */
13 - const OAUTH_STATE_COOKIE = 'sync_basalam_oauth_state';
12 + /** Prefix for the per-user transient holding a pending OAuth authorization. */
13 + const OAUTH_STATE_TRANSIENT = 'sync_basalam_oauth_state_';
14 14
15 15 /** Lifetime of a pending OAuth authorization — the SSO round-trip window. */
16 16 const OAUTH_STATE_TTL = 600; // 10 * MINUTE_IN_SECONDS
17 17
@@ -18,30 +18,18 @@
18 18 /**
19 19 * Remember that the current admin has just started an OAuth authorization.
20 20 *
21 21 * This is called only from the nonce-protected initiation flow, so the
22 - * cookie it stores cannot be planted by a forged cross-site request. The
23 - * callback later requires (and consumes) this cookie, which is what turns
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 24 * the token-saving callback from "always forgeable" into "only valid for a
25 25 * flow this admin actually started".
26 - *
27 - * The proof deliberately lives in a signed HttpOnly cookie instead of a
28 - * WordPress transient. Sites with a persistent object-cache drop-in route
29 - * transients through Redis/Memcached, where a failed write, eviction, or
30 - * cache flush during the OAuth round trip would otherwise invalidate a
31 - * legitimate callback.
32 26 */
33 27 public static function issueOauthState()
34 28 {
35 - $userId = get_current_user_id();
36 - if ($userId <= 0) return false;
29 + $state = wp_generate_password(64, false);
30 + set_transient(self::OAUTH_STATE_TRANSIENT . get_current_user_id(), $state, self::OAUTH_STATE_TTL);
37 31
38 - $state = wp_generate_password(64, false);
39 - $expiresAt = time() + self::OAUTH_STATE_TTL;
40 - $value = self::buildOauthStateCookieValue($state, $userId, $expiresAt);
41 -
42 - if (! self::writeOauthStateCookie($value, $expiresAt)) return false;
43 -
44 32 return $state;
45 33 }
46 34
47 35 /**
@@ -46,112 +34,22 @@
46 34
47 35 /**
48 36 * Validate and consume the pending OAuth authorization for the current user.
49 37 *
50 - * Single use: the cookie is deleted whether or not it was valid, so a
38 + * Single use: the marker is deleted whether or not it was present, so a
51 39 * replayed or forged callback cannot reuse it.
52 40 */
53 41 private static function verifyOauthState()
54 42 {
55 - $value = isset($_COOKIE[self::OAUTH_STATE_COOKIE])
56 - ? (string) wp_unslash($_COOKIE[self::OAUTH_STATE_COOKIE])
57 - : '';
43 + $key = self::OAUTH_STATE_TRANSIENT . get_current_user_id();
44 + $expected = get_transient($key);
45 + delete_transient($key);
58 46
59 - self::clearOauthStateCookie();
60 -
61 - return self::isOauthStateCookieValid(
62 - $value,
63 - get_current_user_id(),
64 - time()
65 - );
66 - }
67 -
68 - private static function buildOauthStateCookieValue($state, $userId, $expiresAt)
69 - {
70 - $payload = json_encode([
71 - 'state' => (string) $state,
72 - 'user_id' => (int) $userId,
73 - 'expires_at' => (int) $expiresAt,
74 - ]);
75 -
76 - if (! is_string($payload)) return '';
77 -
78 - $encodedPayload = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
79 - $signature = hash_hmac('sha256', $encodedPayload, wp_salt('auth'));
80 -
81 - return $encodedPayload . '.' . $signature;
82 - }
83 -
84 - private static function isOauthStateCookieValid($value, $userId, $now)
85 - {
86 - if (! is_string($value) || $value === '' || (int) $userId <= 0) return false;
87 -
88 - $parts = explode('.', $value, 2);
89 - if (count($parts) !== 2) return false;
90 -
91 - [$encodedPayload, $signature] = $parts;
92 - $expectedSignature = hash_hmac('sha256', $encodedPayload, wp_salt('auth'));
93 -
94 - if (! hash_equals($expectedSignature, $signature)) return false;
95 -
96 - $padding = strlen($encodedPayload) % 4;
97 - if ($padding !== 0) $encodedPayload .= str_repeat('=', 4 - $padding);
98 -
99 - $payload = base64_decode(strtr($encodedPayload, '-_', '+/'), true);
100 - $data = is_string($payload) ? json_decode($payload, true) : null;
101 -
102 - if (! is_array($data)) return false;
103 -
104 - return ! empty($data['state'])
105 - && (int) ($data['user_id'] ?? 0) === (int) $userId
106 - && (int) ($data['expires_at'] ?? 0) >= (int) $now;
107 - }
108 -
109 - private static function writeOauthStateCookie($value, $expiresAt)
110 - {
111 - if (! is_string($value) || $value === '' || headers_sent()) return false;
112 -
113 - $written = setcookie(self::OAUTH_STATE_COOKIE, $value, [
114 - 'expires' => (int) $expiresAt,
115 - 'path' => self::oauthStateCookiePath(),
116 - 'domain' => self::oauthStateCookieDomain(),
117 - 'secure' => is_ssl(),
118 - 'httponly' => true,
119 - 'samesite' => 'Lax',
120 - ]);
121 -
122 - // Keep the current request internally consistent for callers and tests.
123 - if ($written) $_COOKIE[self::OAUTH_STATE_COOKIE] = $value;
124 -
125 - return $written;
126 - }
127 -
128 - private static function clearOauthStateCookie()
129 - {
130 - unset($_COOKIE[self::OAUTH_STATE_COOKIE]);
131 -
132 - if (headers_sent()) return;
133 -
134 - setcookie(self::OAUTH_STATE_COOKIE, '', [
135 - 'expires' => time() - HOUR_IN_SECONDS,
136 - 'path' => self::oauthStateCookiePath(),
137 - 'domain' => self::oauthStateCookieDomain(),
138 - 'secure' => is_ssl(),
139 - 'httponly' => true,
140 - 'samesite' => 'Lax',
141 - ]);
142 - }
143 -
144 - private static function oauthStateCookiePath()
145 - {
146 - return defined('ADMIN_COOKIE_PATH') && ADMIN_COOKIE_PATH
147 - ? ADMIN_COOKIE_PATH
148 - : '/wp-admin';
149 - }
150 -
151 - private static function oauthStateCookieDomain()
152 - {
153 - return defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : '';
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);
154 52 }
155 53
156 54 public function getOauthData()
157 55 {