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            if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
123                $this->rest_authentication_status = new WP_Error(
124                    'rest_invalid_request',
125                    __( 'The request method is missing.', 'jetpack-connection' ),
126                    array( 'status' => 400 )
127                );
128                return null;
129            }
130
131            // Only support specific request parameters that have been tested and
132            // are known to work with signature verification.  A different method
133            // can be passed to the WP REST API via the '?_method=' parameter if
134            // needed.
135            if ( 'GET' !== $_SERVER['REQUEST_METHOD'] && 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
136                $this->rest_authentication_status = new WP_Error(
137                    'rest_invalid_request',
138                    __( 'This request method is not supported.', 'jetpack-connection' ),
139                    array( 'status' => 400 )
140                );
141                return null;
142            }
143            if ( 'POST' !== $_SERVER['REQUEST_METHOD'] && ! empty( file_get_contents( 'php://input' ) ) ) {
144                $this->rest_authentication_status = new WP_Error(
145                    'rest_invalid_request',
146                    __( 'This request method does not support body parameters.', 'jetpack-connection' ),
147                    array( 'status' => 400 )
148                );
149                return null;
150            }
151
152            $verified = $this->connection_manager->verify_xml_rpc_signature();
153
154            if (
155                $verified &&
156                isset( $verified['type'] ) &&
157                'blog' === $verified['type']
158            ) {
159                // Site-level authentication successful.
160                $this->rest_authentication_status = true;
161                $this->rest_authentication_type   = 'blog';
162                return null;
163            }
164
165            if (
166                $verified &&
167                isset( $verified['type'] ) &&
168                'user' === $verified['type'] &&
169                ! empty( $verified['user_id'] )
170            ) {
171                // User-level authentication successful.
172                $this->rest_authentication_status = true;
173                $this->rest_authentication_type   = 'user';
174                return $verified['user_id'];
175            }
176
177            // Something else went wrong.  Probably a signature error.
178            $this->rest_authentication_status = new WP_Error(
179                'rest_invalid_signature',
180                __( 'The request is not signed correctly.', 'jetpack-connection' ),
181                array( 'status' => 400 )
182            );
183            return null;
184        } finally {
185            $this->doing_determine_current_user_filter = false;
186        }
187    }
188
189    /**
190     * Report authentication status to the WP REST API.
191     *
192     * @param  WP_Error|mixed $value Error from another authentication handler, null if we should handle it, or another value if not.
193     * @return WP_Error|boolean|null {@see WP_JSON_Server::check_authentication}
194     */
195    public function wp_rest_authentication_errors( $value ) {
196        if ( null !== $value ) {
197            return $value;
198        }
199        return $this->rest_authentication_status;
200    }
201
202    /**
203     * Resets the saved authentication state in between testing requests.
204     */
205    public function reset_saved_auth_state() {
206        $this->rest_authentication_status = null;
207        $this->connection_manager->reset_saved_auth_state();
208    }
209
210    /**
211     * Whether the request was signed with a blog token.
212     *
213     * @since 1.29.0
214     *
215     * @return bool True if the request was signed with a valid blog token, false otherwise.
216     */
217    public static function is_signed_with_blog_token() {
218        $instance = self::init();
219
220        return true === $instance->rest_authentication_status && 'blog' === $instance->rest_authentication_type;
221    }
222
223    /**
224     * Whether the request was signed with a user token.
225     *
226     * @since 6.7.0
227     *
228     * @return bool True if the request was signed with a valid user token, false otherwise.
229     */
230    public static function is_signed_with_user_token() {
231        $instance = self::init();
232
233        return true === $instance->rest_authentication_status && 'user' === $instance->rest_authentication_type;
234    }
235}