Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.50% covered (warning)
87.50%
63 / 72
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Rest_Authentication
87.50% covered (warning)
87.50%
63 / 72
71.43% covered (warning)
71.43%
5 / 7
30.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 wp_rest_authenticate
87.72% covered (warning)
87.72%
50 / 57
0.00% covered (danger)
0.00%
0 / 1
19.67
 wp_rest_authentication_errors
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 reset_saved_auth_state
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_signed_with_blog_token
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_signed_with_user_token
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * The Jetpack Connection Rest Authentication file.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use WP_Error;
11
12/**
13 * The Jetpack Connection Rest Authentication class.
14 */
15class Rest_Authentication {
16
17    /**
18     * The rest authentication status.
19     *
20     * @since 1.17.0
21     * @var boolean
22     */
23    private $rest_authentication_status = null;
24
25    /**
26     * The rest authentication type.
27     * Can be either 'user' or 'blog' depending on whether the request
28     * is signed with a user or a blog token.
29     *
30     * @since 1.29.0
31     * @var string
32     */
33    private $rest_authentication_type = null;
34
35    /**
36     * The Manager object.
37     *
38     * @since 1.17.0
39     * @var Object
40     */
41    private $connection_manager = null;
42
43    /**
44     * Holds the singleton instance of this class
45     *
46     * @since 1.17.0
47     * @var Object
48     */
49    private static $instance = false;
50
51    /**
52     * Flag used to avoid determine_current_user filter to enter an infinite loop
53     *
54     * @since 1.26.0
55     * @var boolean
56     */
57    private $doing_determine_current_user_filter = false;
58
59    /**
60     * The constructor.
61     */
62    private function __construct() {
63        $this->connection_manager = new Manager();
64    }
65
66    /**
67     * Controls the single instance of this class.
68     *
69     * @static
70     */
71    public static function init() {
72        if ( ! self::$instance ) {
73            self::$instance = new self();
74
75            add_filter( 'determine_current_user', array( self::$instance, 'wp_rest_authenticate' ) );
76            add_filter( 'rest_authentication_errors', array( self::$instance, 'wp_rest_authentication_errors' ) );
77        }
78
79        return self::$instance;
80    }
81
82    /**
83     * Authenticates requests from Jetpack server to WP REST API endpoints.
84     * Uses the existing XMLRPC request signing implementation.
85     *
86     * @param int|bool $user User ID if one has been determined, false otherwise.
87     *
88     * @return int|null The user id or null if the request was authenticated via blog token, or not authenticated at all.
89     */
90    public function wp_rest_authenticate( $user ) {
91        if ( $this->doing_determine_current_user_filter ) {
92            return $user;
93        }
94
95        $this->doing_determine_current_user_filter = true;
96
97        try {
98            if ( ! empty( $user ) ) {
99                // Another authentication method is in effect.
100                return $user;
101            }
102
103            add_filter(
104                'jetpack_constant_default_value',
105                __NAMESPACE__ . '\Utils::jetpack_api_constant_filter',
106                10,
107                2
108            );
109
110            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
111            if ( ! isset( $_GET['_for'] ) || 'jetpack' !== $_GET['_for'] ) {
112                // Nothing to do for this authentication method.
113                return null;
114            }
115
116            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
117            if ( ! isset( $_GET['token'] ) && ! isset( $_GET['signature'] ) ) {
118                // Nothing to do for this authentication method.
119                return null;
120            }
121
122            // These `rest_invalid_request` errors occur before `verify_xml_rpc_signature()`,
123            // so the request has not been authenticated as WP.com. Do not report them to
124            // Error_Handler: they are malformed unauthenticated requests, not connection errors.
125            if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
126                $this->rest_authentication_status = new WP_Error(
127                    'rest_invalid_request',
128                    __( 'The request method is missing.', 'jetpack-connection' ),
129                    array( 'status' => 400 )
130                );
131                return null;
132            }
133
134            // Only support specific request parameters that have been tested and
135            // are known to work with signature verification.  A different method
136            // can be passed to the WP REST API via the '?_method=' parameter if
137            // needed.
138            if ( 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
139                $this->rest_authentication_status = new WP_Error(
140                    'rest_invalid_request',
141                    __( 'This request method is not supported.', 'jetpack-connection' ),
142                    array( 'status' => 400 )
143                );
144                return null;
145            }
146            if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) {
147                $this->rest_authentication_status = new WP_Error(
148                    'rest_invalid_request',
149                    __( 'This request method does not support body parameters.', 'jetpack-connection' ),
150                    array( 'status' => 400 )
151                );
152                return null;
153            }
154
155            $verified = $this->connection_manager->verify_xml_rpc_signature();
156
157            if (
158                $verified &&
159                isset( $verified['type'] ) &&
160                'blog' === $verified['type']
161            ) {
162                // Site-level authentication successful.
163                $this->rest_authentication_status = true;
164                $this->rest_authentication_type   = 'blog';
165                return null;
166            }
167
168            if (
169                $verified &&
170                isset( $verified['type'] ) &&
171                'user' === $verified['type'] &&
172                ! empty( $verified['user_id'] )
173            ) {
174                // User-level authentication successful.
175                $this->rest_authentication_status = true;
176                $this->rest_authentication_type   = 'user';
177                return $verified['user_id'];
178            }
179
180            // Something else went wrong.  Probably a signature error.
181            $this->rest_authentication_status = new WP_Error(
182                'rest_invalid_signature',
183                __( 'The request is not signed correctly.', 'jetpack-connection' ),
184                array( 'status' => 400 )
185            );
186            return null;
187        } finally {
188            $this->doing_determine_current_user_filter = false;
189        }
190    }
191
192    /**
193     * Report authentication status to the WP REST API.
194     *
195     * @param  WP_Error|mixed $value Error from another authentication handler, null if we should handle it, or another value if not.
196     * @return WP_Error|boolean|null {@see WP_JSON_Server::check_authentication}
197     */
198    public function wp_rest_authentication_errors( $value ) {
199        if ( null !== $value ) {
200            return $value;
201        }
202        return $this->rest_authentication_status;
203    }
204
205    /**
206     * Resets the saved authentication state in between testing requests.
207     */
208    public function reset_saved_auth_state() {
209        $this->rest_authentication_status = null;
210        $this->connection_manager->reset_saved_auth_state();
211    }
212
213    /**
214     * Whether the request was signed with a blog token.
215     *
216     * @since 1.29.0
217     *
218     * @return bool True if the request was signed with a valid blog token, false otherwise.
219     */
220    public static function is_signed_with_blog_token() {
221        $instance = self::init();
222
223        return true === $instance->rest_authentication_status && 'blog' === $instance->rest_authentication_type;
224    }
225
226    /**
227     * Whether the request was signed with a user token.
228     *
229     * @since 6.7.0
230     *
231     * @return bool True if the request was signed with a valid user token, false otherwise.
232     */
233    public static function is_signed_with_user_token() {
234        $instance = self::init();
235
236        return true === $instance->rest_authentication_status && 'user' === $instance->rest_authentication_type;
237    }
238}