-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathclass-authorize.php
More file actions
341 lines (310 loc) · 9.77 KB
/
class-authorize.php
File metadata and controls
341 lines (310 loc) · 9.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<?php
/**
* IndieAuth Authorize class file.
*
* @package IndieAuth
*/
namespace IndieAuth;
use IndieAuth\Token\User as Token_User;
/**
* IndieAuth Authorize class.
*
* Handles token verification and authentication for IndieAuth.
*
* @since 1.0.0
*/
class Authorize {
/**
* Error object.
*
* @var \WP_Error|OAuth_Response|null
*/
public $error = null;
/**
* Current scopes.
*
* @var array
*/
public $scopes = array();
/**
* Response data.
*
* @var array
*/
public $response = array();
/**
* Constructor.
*
* @param bool $load Whether to load hooks.
*/
public function __construct( $load = true ) {
// Load the hooks for this class only if true. This allows for debugging of the functions.
if ( true === $load ) {
$this->load();
}
}
/**
* Load hooks.
*/
public function load() {
// Do not call in CLI environment.
if ( defined( 'WP_CLI' ) ) {
return;
}
// WordPress validates the auth cookie at priority 10 and this cannot be overridden by an earlier priority.
// It validates the logged in cookie at 20 and can be overridden by something with a higher priority.
\add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 15 );
\add_filter( 'rest_authentication_errors', array( $this, 'rest_authentication_errors' ) );
\add_filter( 'indieauth_scopes', array( $this, 'get_indieauth_scopes' ), 9 );
\add_filter( 'indieauth_response', array( $this, 'get_indieauth_response' ), 9 );
\add_filter( 'wp_rest_server_class', array( $this, 'wp_rest_server_class' ) );
\add_filter( 'rest_request_after_callbacks', array( $this, 'return_oauth_error' ), 10, 3 );
}
/**
* Ensures responses to any IndieAuth endpoints are always OAuth Responses rather than WP_Error.
*
* @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
* @param array $handler Route handler used for the request.
* @param \WP_REST_Request $request Request used to generate the response.
* @return \WP_REST_Response|OAuth_Response|mixed Modified response.
*/
public static function return_oauth_error( $response, $handler, $request ) {
if ( 0 !== strpos( $request->get_route(), '/indieauth/1.0/' ) ) {
return $response;
}
if ( \is_wp_error( $response ) ) {
return wp_error_to_oauth_response( $response );
}
return $response;
}
/**
* Prevent caching of unauthenticated status. See comment below.
*
* We don't actually care about the `wp_rest_server_class` filter, it just
* happens right after the constant we do care about is defined. This is taken from the Application Passwords plugin.
*
* @param string $class REST server class name.
* @return string REST server class name.
*/
public static function wp_rest_server_class( $class ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.classFound
global $current_user;
if ( defined( 'REST_REQUEST' ) && REST_REQUEST && $current_user instanceof \WP_User && 0 === $current_user->ID ) {
/*
* For our authentication to work, we need to remove the cached lack
* of a current user, so the next time it checks, we can detect that
* this is a rest api request and allow our override to happen. This
* is because the constant is defined later than the first get current
* user call may run.
*/
$current_user = null; // phpcs:ignore
}
return $class;
}
/**
* Get IndieAuth scopes.
*
* @param array $scopes Existing scopes.
* @return array Scopes.
*/
public function get_indieauth_scopes( $scopes ) {
return $scopes ? $scopes : $this->scopes;
}
/**
* Get IndieAuth response.
*
* @param array $response Existing response.
* @return array Response.
*/
public function get_indieauth_response( $response ) {
return $response ? $response : $this->response;
}
/**
* Report our errors, if we have any.
*
* Attached to the rest_authentication_errors filter. Passes through existing
* errors registered on the filter.
*
* @param \WP_Error|null|true $error Current error, null or true.
* @return \WP_Error|null|true Error if one is set, unchanged otherwise.
*/
public function rest_authentication_errors( $error = null ) {
if ( \is_user_logged_in() ) {
// Another OAuth2 plugin successfully authenticated.
return $error;
}
if ( \is_wp_error( $this->error ) ) {
return $this->error;
}
if ( is_oauth_error( $this->error ) ) {
return $this->error->to_wp_error();
}
return $error;
}
/**
* Uses an IndieAuth token to authenticate to WordPress.
*
* @param int|bool $user_id User ID if one has been determined otherwise false.
*
* @return int|bool User ID otherwise false.
*/
public function determine_current_user( $user_id ) {
$token = $this->get_provided_token();
// If there is not a token that means this is not an attempt to log in using IndieAuth.
if ( ! isset( $token ) ) {
return $user_id;
}
// If there is a token and it is invalid then reject all logins.
$params = $this->verify_access_token( $token );
if ( ! isset( $params ) ) {
return $user_id;
}
if ( is_oauth_error( $params ) ) {
$this->error = $params;
return $user_id;
}
if ( is_array( $params ) ) {
// If this is a token auth response and not a test run, add this constant.
if ( ! function_exists( 'tests_add_filter' ) ) {
define( 'INDIEAUTH_TOKEN', true );
}
$this->response = $params;
$this->scopes = explode( ' ', $params['scope'] );
// The User ID must be passed in the request.
if ( isset( $params['user'] ) ) {
return (int) $params['user'];
}
}
$this->error = new OAuth_Response(
'unauthorized',
\__( 'User Not Found on this Site', 'indieauth' ),
401,
array(
'response' => $params,
)
);
return $user_id;
}
/**
* Get the authorization header
*
* On certain systems and configurations, the Authorization header will be
* stripped out by the server or PHP. Typically this is then used to
* generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use
* `getallheaders` here to try and grab it out instead.
*
* @return string|null Authorization header if set, null otherwise
*/
public function get_authorization_header() {
$auth = null;
if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
$auth = \wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
} elseif ( ! empty( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
// When Apache speaks via FastCGI with PHP, then the authorization header is often available as REDIRECT_HTTP_AUTHORIZATION.
$auth = \wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
} else {
$headers = getallheaders();
// Check for the authorization header case-insensitively.
foreach ( $headers as $key => $value ) {
if ( strtolower( $key ) === 'authorization' ) {
$auth = \wp_unslash( $value );
break;
}
}
}
return $auth;
}
/**
* Extracts the token from the authorization header or the current request.
*
* @return string|null Token on success, null on failure.
*/
public function get_provided_token() {
$header = $this->get_authorization_header();
if ( isset( $header ) ) {
$token = $this->get_token_from_bearer_header( $header );
if ( isset( $token ) ) {
return $token;
}
}
$token = $this->get_token_from_request();
if ( isset( $token ) ) {
return $token;
}
return null;
}
/**
* Extracts the token from the given authorization header.
*
* @param string $header Authorization header.
*
* @return string|null Token on success, null on failure.
*/
public function get_token_from_bearer_header( $header ) {
if ( is_string( $header ) && preg_match( '/Bearer ([\x20-\x7E]+)/', trim( $header ), $matches ) ) {
return $matches[1];
}
return null;
}
/**
* Extracts the token from the current request.
*
* @return string|null Token on success, null on failure.
*/
public function get_token_from_request() {
if ( empty( $_POST['access_token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return null;
}
$token = \sanitize_text_field( \wp_unslash( $_POST['access_token'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( is_string( $token ) ) {
return $token;
}
return null;
}
/**
* Verifies Access Token.
*
* @param string $token The token to verify.
* @return array|OAuth_Response Return either the token information or an OAuth Error Object.
*/
public function verify_access_token( $token ) {
$tokens = new Token_User( '_indieauth_token_' );
$return = $tokens->get( $token );
if ( empty( $return ) ) {
return new OAuth_Response(
'invalid_token',
\__( 'Invalid access token', 'indieauth' ),
401
);
}
if ( is_oauth_error( $return ) ) {
return $return;
}
$return['last_accessed'] = time();
$return['last_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
$tokens->update( $token, $return );
if ( array_key_exists( 'exp', $return ) ) {
$return['expires_in'] = $return['exp'] - time();
}
return $return;
}
/**
* Verifies authorization code.
*
* @param string $code Authorization Code.
* @return array|OAuth_Response Return either the code information or an OAuth Error object.
*/
public static function verify_authorization_code( $code ) {
$tokens = new Token_User( '_indieauth_code_' );
$return = $tokens->get( $code );
if ( empty( $return ) ) {
return new OAuth_Response(
'invalid_code',
\__( 'Invalid authorization code', 'indieauth' ),
401
);
}
// Once the code is verified destroy it.
$tokens->destroy( $code );
return $return;
}
}