· 6 years ago · Apr 10, 2020, 05:00 PM
1<?php
2// Exit if accessed directly
3if (
4 !defined('ABSPATH') ||
5 !defined('WP_API_CONSUMER_KEY') ||
6 !defined('WP_API_CONSUMER_SECRET') ||
7 !defined('WP_API_SERVICE_USER_ID')
8) exit;
9
10/**
11 * REST API authentication class.
12 */
13class WP_REST_Authentication {
14
15 /**
16 * Authentication error.
17 *
18 * @var WP_Error
19 */
20 protected $error = null;
21
22 /**
23 * Authentication error.
24 *
25 * @var []string
26 */
27 protected $allowed_ips = [];
28
29 /**
30 * Initialize authentication actions.
31 */
32 public function __construct() {
33 add_filter('determine_current_user', [$this, 'authenticate'], 15);
34 add_filter('rest_authentication_errors', [$this, 'check_authentication_error'], 15);
35 add_filter('rest_post_dispatch', [$this, 'send_unauthorized_headers'], 50);
36
37 if (defined(WP_API_ALLOWED_IPS)) {
38 $this->allowed_ips = array_map('trim', explode(',', WP_API_ALLOWED_IPS));
39 }
40 }
41
42 protected function is_ip_valid() {
43 if (count($this->allowed_ips) === 0) {
44 // Skipping IP check if none defined
45 return true;
46 }
47
48 $user_ip = $_SERVER['REMOTE_ADDR'];
49 if (!empty($user_ip) && in_array($user_ip, $this->allowed_ips)) {
50 return true;
51 }
52
53 if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
54 $proxies = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
55 if (count($proxies) > 0) foreach ($proxies as $proxy) {
56 if in_array(trim($proxy), $this->allowed_ips) {
57 return true;
58 }
59 }
60 }
61
62 return false;
63 }
64
65 /**
66 * Check if is request to our REST API.
67 *
68 * @return bool
69 */
70 protected function is_request_to_rest_api() {
71 if (empty($_SERVER['REQUEST_URI'])) {
72 return false;
73 }
74
75 $rest_prefix = trailingslashit(rest_get_url_prefix());
76 $request_uri = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI']));
77 return (strpos($request_uri, $rest_prefix) === 0);
78 }
79
80 /**
81 * Authenticate user.
82 *
83 * @param int|false $user_id User ID if one has been determined, false otherwise.
84 * @return int|false
85 */
86 public function authenticate($user_id) {
87 // Do not authenticate twice and check if is a request to our endpoint in the WP REST API.
88 if (empty($user_id) && is_ssl() && $this->is_ip_valid() && $this->is_request_to_rest_api()) {
89 $user_id = $this->perform_basic_authentication();
90 }
91
92 return $user_id;
93 }
94
95 /**
96 * Check for authentication error.
97 *
98 * @param WP_Error|null|bool $error Error data.
99 * @return WP_Error|null|bool
100 */
101 public function check_authentication_error($error) {
102 if (empty($error)) {
103 $error = $this->get_error();
104 }
105
106 return $error;
107 }
108
109 /**
110 * Set authentication error.
111 *
112 * @param WP_Error $error Authentication error data.
113 */
114 protected function set_error($error) {
115 $this->error = $error;
116 }
117
118 /**
119 * Get authentication error.
120 *
121 * @return WP_Error|null.
122 */
123 protected function get_error() {
124 return $this->error;
125 }
126
127 /**
128 * Basic Authentication.
129 *
130 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
131 * attacks, so the request can be authenticated by simply looking up the user
132 * associated with the given consumer key and confirming the consumer secret
133 * provided is valid.
134 *
135 * @return int|bool
136 */
137 private function perform_basic_authentication() {
138 $consumer_key = '';
139 $consumer_secret = '';
140
141 // If the $_GET parameters are present, use those first.
142 if (!empty($_GET['consumer_key']) && !empty($_GET['consumer_secret'])) {
143 $consumer_key = $_GET['consumer_key'];
144 $consumer_secret = $_GET['consumer_secret'];
145 }
146
147 // If the above is not present, we will do full basic auth.
148 if (!$consumer_key && !empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
149 $consumer_key = $_SERVER['PHP_AUTH_USER'];
150 $consumer_secret = $_SERVER['PHP_AUTH_PW'];
151 }
152
153 // Stop if don't have any key.
154 if (empty($consumer_key) || empty($consumer_secret)) {
155 return false;
156 }
157
158 if ($consumer_key !== WP_API_CONSUMER_KEY) {
159 return false;
160 }
161
162 // Validate user secret
163 if (!hash_equals($consumer_secret, WP_API_CONSUMER_SECRET)) {
164 $error = new WP_Error('wordpress_rest_authentication_error', 'Consumer secret is invalid');
165 $this->set_error($error, ['status' => 401]);
166
167 return false;
168 }
169
170 return WP_API_SERVICE_USER_ID;
171 }
172
173 /**
174 * If the consumer_key and consumer_secret $_GET parameters are NOT provided
175 * and the Basic auth headers are either not present or the consumer secret does not match the consumer
176 * key provided, then return the correct Basic headers and an error message.
177 *
178 * @param WP_REST_Response $response Current response being served.
179 * @return WP_REST_Response
180 */
181 public function send_unauthorized_headers($response) {
182 if (is_wp_error($this->get_error())) {
183 $auth_message = 'Use consumer key in the username and consumer secret as passowrd.';
184 $response->header('WWW-Authenticate', "Basic realm=\"$auth_message\"", true);
185 }
186
187 return $response;
188 }
189}
190
191new WP_REST_Authentication();