PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.16
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / API / Auth.php

Auth.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.16, at includes/API/Auth.php

584 lines 16.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * POS Auth API.
4 *
5 * @author Paul Kilmurray <paul@kilbot.com>
6 *
7 * @see http://wcpos.com
8 * @package WCPOS\WooCommercePOS
9 */
10
11 namespace WCPOS\WooCommercePOS\API;
12
13 use WCPOS\WooCommercePOS\Services\Auth as AuthService;
14 use WP_REST_Controller;
15 use WP_REST_Request;
16 use WP_REST_Response;
17 use WP_REST_Server;
18 use const WCPOS\WooCommercePOS\SHORT_NAME;
19
20 /**
21 * Auth class.
22 */
23 class Auth extends WP_REST_Controller {
24 /**
25 * Endpoint namespace.
26 *
27 * @var string
28 */
29 protected $namespace = SHORT_NAME . '/v1';
30
31 /**
32 * Route base.
33 *
34 * @var string
35 */
36 protected $rest_base = 'auth';
37
38 /**
39 * Stores constructor.
40 */
41 public function __construct() {
42 }
43
44 /**
45 * Register the routes for the auth controller.
46 */
47 public function register_routes(): void {
48 // Test authorization method support (public endpoint).
49 register_rest_route(
50 $this->namespace,
51 '/' . $this->rest_base . '/test',
52 array(
53 'methods' => WP_REST_Server::READABLE,
54 'callback' => array( $this, 'test_authorization' ),
55 'permission_callback' => '__return_true', // Public endpoint - no authentication required.
56 )
57 );
58
59 // Refresh access token using refresh token.
60 register_rest_route(
61 $this->namespace,
62 '/' . $this->rest_base . '/refresh',
63 array(
64 'methods' => WP_REST_Server::CREATABLE,
65 'callback' => array( $this, 'refresh_token' ),
66 'permission_callback' => '__return_true', // Public endpoint - validates refresh token internally.
67 'args' => array(
68 'refresh_token' => array(
69 'description' => __( 'The refresh token to use for generating a new access token.', 'woocommerce-pos' ),
70 'type' => 'string',
71 'required' => true,
72 ),
73 ),
74 )
75 );
76
77 // Get user sessions.
78 register_rest_route(
79 $this->namespace,
80 '/' . $this->rest_base . '/sessions',
81 array(
82 'methods' => WP_REST_Server::READABLE,
83 'callback' => array( $this, 'get_sessions' ),
84 'permission_callback' => array( $this, 'check_session_permissions' ),
85 'args' => array(
86 'user_id' => array(
87 'description' => __( 'The user ID to get sessions for. Defaults to current user.', 'woocommerce-pos' ),
88 'type' => 'integer',
89 'required' => false,
90 'validate_callback' => function ( $param ) {
91 return is_numeric( $param );
92 },
93 ),
94 ),
95 )
96 );
97
98 // Delete all sessions or all except current.
99 register_rest_route(
100 $this->namespace,
101 '/' . $this->rest_base . '/sessions',
102 array(
103 'methods' => WP_REST_Server::DELETABLE,
104 'callback' => array( $this, 'delete_all_sessions' ),
105 'permission_callback' => array( $this, 'check_session_permissions' ),
106 'args' => array(
107 'user_id' => array(
108 'description' => __( 'The user ID to delete sessions for.', 'woocommerce-pos' ),
109 'type' => 'integer',
110 'required' => true,
111 'validate_callback' => function ( $param ) {
112 return is_numeric( $param );
113 },
114 ),
115 'except_current' => array(
116 'description' => __( 'Whether to keep the current session.', 'woocommerce-pos' ),
117 'type' => 'boolean',
118 'required' => false,
119 'default' => false,
120 ),
121 ),
122 )
123 );
124
125 // Delete specific session by JTI.
126 register_rest_route(
127 $this->namespace,
128 '/' . $this->rest_base . '/sessions/(?P<jti>[a-f0-9\-]+)',
129 array(
130 'methods' => WP_REST_Server::DELETABLE,
131 'callback' => array( $this, 'delete_session' ),
132 'permission_callback' => array( $this, 'check_session_permissions' ),
133 'args' => array(
134 'jti' => array(
135 'description' => __( 'The session JTI to delete.', 'woocommerce-pos' ),
136 'type' => 'string',
137 'required' => true,
138 'validate_callback' => function ( $param ) {
139 // Validate UUID format.
140 return preg_match( '/^[a-f0-9\-]{36}$/i', $param );
141 },
142 ),
143 'user_id' => array(
144 'description' => __( 'The user ID that owns the session.', 'woocommerce-pos' ),
145 'type' => 'integer',
146 'required' => true,
147 'validate_callback' => function ( $param ) {
148 return is_numeric( $param );
149 },
150 ),
151 ),
152 )
153 );
154
155 // Get all users with active sessions (admin/manager only).
156 register_rest_route(
157 $this->namespace,
158 '/' . $this->rest_base . '/users/sessions',
159 array(
160 'methods' => WP_REST_Server::READABLE,
161 'callback' => array( $this, 'get_all_users_sessions' ),
162 'permission_callback' => array( $this, 'check_admin_permissions' ),
163 )
164 );
165 }
166
167
168 /**
169 * Test authorization method endpoint.
170 *
171 * This public endpoint tests whether the server supports Authorization headers
172 * or requires query parameters for authorization. This is important because
173 * some servers block Authorization headers for security reasons.
174 *
175 * @param WP_REST_Request $request The REST request object.
176 *
177 * @return WP_REST_Response
178 */
179 public function test_authorization( WP_REST_Request $request ): WP_REST_Response {
180 // Check for Authorization header.
181 $header_auth = $request->get_header( 'authorization' );
182 $has_header_auth = ! empty( $header_auth );
183
184 // Check for authorization query parameter.
185 $param_auth = $request->get_param( 'authorization' );
186 $has_param_auth = ! empty( $param_auth );
187
188 // Only return success if we received authorization via at least one method.
189 if ( ! $has_header_auth && ! $has_param_auth ) {
190 return rest_ensure_response(
191 array(
192 'status' => 'error',
193 'message' => 'No authorization token detected',
194 )
195 );
196 }
197
198 $response_data = array(
199 'status' => 'success',
200 'message' => 'Authorization token detected successfully',
201 );
202
203 // Add authorization details (mask values to avoid leaking tokens in logs).
204 $response_data['received_header_auth'] = $has_header_auth;
205 if ( $has_header_auth ) {
206 $response_data['header_length'] = \strlen( $header_auth );
207 }
208
209 $response_data['received_param_auth'] = $has_param_auth;
210 if ( $has_param_auth ) {
211 $response_data['param_length'] = \strlen( $param_auth );
212 }
213
214 // Indicate which method was used.
215 if ( $has_header_auth && $has_param_auth ) {
216 $response_data['auth_method'] = 'both';
217 } elseif ( $has_header_auth ) {
218 $response_data['auth_method'] = 'header';
219 } else {
220 $response_data['auth_method'] = 'param';
221 }
222
223 return rest_ensure_response( $response_data );
224 }
225
226 /**
227 * Refresh access token using a valid refresh token.
228 *
229 * This endpoint allows clients to obtain a new access token using a valid refresh token.
230 * Compatible with the axios-auth-refresh library and follows OAuth 2.0 refresh token flow.
231 *
232 * @param WP_REST_Request $request The REST request object.
233 *
234 * @return WP_REST_Response
235 */
236 public function refresh_token( WP_REST_Request $request ): WP_REST_Response {
237 $refresh_token = $request->get_param( 'refresh_token' );
238
239 if ( empty( $refresh_token ) ) {
240 return rest_ensure_response(
241 array(
242 'error' => 'invalid_request',
243 'error_description' => 'Missing refresh_token parameter',
244 ),
245 400
246 );
247 }
248
249 $auth_service = AuthService::instance();
250 $result = $auth_service->refresh_access_token( $refresh_token );
251
252 if ( is_wp_error( $result ) ) {
253 $error_code = $result->get_error_code();
254 $error_msg = $result->get_error_message();
255 $status = $result->get_error_data()['status'] ?? 400;
256
257 // Map error codes to OAuth 2.0 standard error responses.
258 $oauth_error = 'invalid_grant'; // Default OAuth error for refresh token issues.
259
260 if ( false !== strpos( $error_code, 'invalid_token' ) || false !== strpos( $error_code, 'revoked' ) ) {
261 $oauth_error = 'invalid_grant';
262 } elseif ( false !== strpos( $error_code, 'user_not_found' ) ) {
263 $oauth_error = 'invalid_grant';
264 }
265
266 return rest_ensure_response(
267 array(
268 'error' => $oauth_error,
269 'error_description' => $error_msg,
270 ),
271 $status
272 );
273 }
274
275 // Calculate expires_in for axios-auth-refresh compatibility.
276 $current_time = time();
277 $expires_in = max( 0, $result['expires_at'] - $current_time );
278
279 // Return response in format compatible with axios-auth-refresh.
280 $response_data = array(
281 'access_token' => $result['access_token'],
282 'token_type' => $result['token_type'],
283 'expires_in' => $expires_in,
284 'expires_at' => $result['expires_at'],
285 );
286
287 return rest_ensure_response( $response_data );
288 }
289
290 /**
291 * Get sessions for a user.
292 *
293 * @param WP_REST_Request $request The REST request object.
294 *
295 * @return WP_REST_Response
296 */
297 public function get_sessions( WP_REST_Request $request ): WP_REST_Response {
298 $user_id = $request->get_param( 'user_id' );
299
300 // Default to current user if not specified.
301 if ( empty( $user_id ) ) {
302 $user_id = get_current_user_id();
303 }
304
305 $auth_service = AuthService::instance();
306 $sessions = $auth_service->get_user_sessions( (int) $user_id );
307
308 // Get current JTI if available from the request token.
309 $current_jti = $this->get_current_jti_from_request( $request );
310
311 // Mark the current session.
312 foreach ( $sessions as &$session ) {
313 $session['is_current'] = ( ! empty( $current_jti ) && $session['jti'] === $current_jti );
314 }
315
316 return rest_ensure_response(
317 array(
318 'user_id' => $user_id,
319 'sessions' => $sessions,
320 )
321 );
322 }
323
324 /**
325 * Delete a specific session.
326 *
327 * @param WP_REST_Request $request The REST request object.
328 *
329 * @return WP_REST_Response
330 */
331 public function delete_session( WP_REST_Request $request ): WP_REST_Response {
332 $jti = $request->get_param( 'jti' );
333 $user_id = $request->get_param( 'user_id' );
334
335 if ( empty( $jti ) || empty( $user_id ) ) {
336 return rest_ensure_response(
337 array(
338 'success' => false,
339 'message' => /* translators: REST API schema field label or error message. */ __( 'Missing required parameters.', 'woocommerce-pos' ),
340 ),
341 400
342 );
343 }
344
345 $auth_service = AuthService::instance();
346
347 // Revoke session and blacklist it - this invalidates all access tokens for this session.
348 $result = $auth_service->revoke_session_with_blacklist( (int) $user_id, $jti );
349
350 if ( $result ) {
351 return rest_ensure_response(
352 array(
353 'success' => true,
354 'message' => /* translators: REST API schema field label or error message. */ __( 'Session revoked successfully.', 'woocommerce-pos' ),
355 )
356 );
357 }
358
359 return rest_ensure_response(
360 array(
361 'success' => false,
362 'message' => __( 'Failed to revoke session.', 'woocommerce-pos' ),
363 ),
364 404
365 );
366 }
367
368 /**
369 * Delete all sessions for a user.
370 *
371 * @param WP_REST_Request $request The REST request object.
372 *
373 * @return WP_REST_Response
374 */
375 public function delete_all_sessions( WP_REST_Request $request ): WP_REST_Response {
376 $user_id = $request->get_param( 'user_id' );
377 $except_current = $request->get_param( 'except_current' );
378
379 if ( empty( $user_id ) ) {
380 return rest_ensure_response(
381 array(
382 'success' => false,
383 'message' => /* translators: REST API schema field label or error message. */ __( 'Missing user_id parameter.', 'woocommerce-pos' ),
384 ),
385 400
386 );
387 }
388
389 $auth_service = AuthService::instance();
390
391 if ( $except_current ) {
392 // Get current JTI from request.
393 $current_jti = $this->get_current_jti_from_request( $request );
394
395 if ( empty( $current_jti ) ) {
396 return rest_ensure_response(
397 array(
398 'success' => false,
399 'message' => __( 'Could not determine current session.', 'woocommerce-pos' ),
400 ),
401 400
402 );
403 }
404
405 $result = $auth_service->revoke_all_sessions_except( (int) $user_id, $current_jti );
406 } else {
407 $result = $auth_service->revoke_all_refresh_tokens( (int) $user_id );
408 }
409
410 if ( $result ) {
411 return rest_ensure_response(
412 array(
413 'success' => true,
414 'message' => /* translators: REST API schema field label or error message. */ __( 'Sessions revoked successfully.', 'woocommerce-pos' ),
415 )
416 );
417 }
418
419 return rest_ensure_response(
420 array(
421 'success' => false,
422 'message' => __( 'Failed to revoke sessions.', 'woocommerce-pos' ),
423 ),
424 500
425 );
426 }
427
428 /**
429 * Get all users with active sessions (admin/manager only).
430 *
431 * @param WP_REST_Request $request The REST request object.
432 *
433 * @return WP_REST_Response
434 */
435 public function get_all_users_sessions( WP_REST_Request $request ): WP_REST_Response {
436 global $wpdb;
437
438 $auth_service = AuthService::instance();
439
440 // Get all users who have refresh tokens.
441 $user_ids = $wpdb->get_col(
442 "SELECT DISTINCT user_id
443 FROM {$wpdb->usermeta}
444 WHERE meta_key = '_woocommerce_pos_refresh_tokens'"
445 );
446
447 $users_data = array();
448
449 // Get current JTI if available from the request token.
450 $current_jti = $this->get_current_jti_from_request( $request );
451
452 foreach ( $user_ids as $user_id ) {
453 $user = get_user_by( 'id', $user_id );
454 if ( ! $user ) {
455 continue;
456 }
457
458 $sessions = $auth_service->get_user_sessions( (int) $user_id );
459
460 // Only include users with active sessions.
461 if ( empty( $sessions ) ) {
462 continue;
463 }
464
465 // Mark the current session and find the most recent activity.
466 $last_active = 0;
467 foreach ( $sessions as &$session ) {
468 $session['is_current'] = ( ! empty( $current_jti ) && $session['jti'] === $current_jti );
469 if ( $session['last_active'] > $last_active ) {
470 $last_active = $session['last_active'];
471 }
472 }
473 unset( $session );
474
475 $users_data[] = array(
476 'user_id' => (int) $user_id,
477 'username' => $user->user_login,
478 'display_name' => $user->display_name,
479 'avatar_url' => get_avatar_url( $user_id, array( 'size' => 96 ) ),
480 'session_count' => \count( $sessions ),
481 'last_active' => $last_active,
482 'sessions' => $sessions,
483 );
484 }
485
486 // Sort by last_active descending (most recent first).
487 usort(
488 $users_data,
489 function ( $a, $b ) {
490 return $b['last_active'] - $a['last_active'];
491 }
492 );
493
494 return rest_ensure_response(
495 array(
496 'users' => $users_data,
497 'total' => \count( $users_data ),
498 )
499 );
500 }
501
502 /**
503 * Check session management permissions.
504 *
505 * @param WP_REST_Request $request The REST request object.
506 *
507 * @return bool
508 */
509 public function check_session_permissions( WP_REST_Request $request ): bool {
510 // User must be logged in.
511 if ( ! is_user_logged_in() ) {
512 return false;
513 }
514
515 $target_user_id = $request->get_param( 'user_id' );
516
517 // Default to current user if not specified (for GET requests).
518 if ( empty( $target_user_id ) ) {
519 $target_user_id = get_current_user_id();
520 }
521
522 $auth_service = AuthService::instance();
523
524 return $auth_service->can_manage_user_sessions( (int) $target_user_id );
525 }
526
527 /**
528 * Check admin/manager permissions.
529 *
530 * @param WP_REST_Request $request The REST request object.
531 *
532 * @return bool
533 */
534 public function check_admin_permissions( WP_REST_Request $request ): bool {
535 // Only administrators and shop managers.
536 return current_user_can( 'manage_options' ) || current_user_can( 'manage_woocommerce' );
537 }
538
539 /**
540 * Get current JTI from the request's authorization token.
541 *
542 * @param WP_REST_Request $request The REST request object.
543 *
544 * @return null|string
545 */
546 private function get_current_jti_from_request( WP_REST_Request $request ): ?string {
547 // Try to get the token from Authorization header.
548 $auth_header = $request->get_header( 'authorization' );
549
550 if ( empty( $auth_header ) ) {
551 // Try query parameter.
552 $auth_header = $request->get_param( 'authorization' );
553 }
554
555 if ( empty( $auth_header ) ) {
556 return null;
557 }
558
559 // Extract token from "Bearer TOKEN".
560 $token = str_replace( 'Bearer ', '', $auth_header );
561
562 if ( empty( $token ) ) {
563 return null;
564 }
565
566 // Try to decode as refresh token first.
567 $auth_service = AuthService::instance();
568 $decoded = $auth_service->validate_token( $token, 'refresh' );
569
570 if ( ! is_wp_error( $decoded ) ) {
571 return $decoded->jti ?? null;
572 }
573
574 // Fallback: access tokens carry the session link as refresh_jti.
575 $decoded = $auth_service->validate_token( $token, 'access' );
576
577 if ( is_wp_error( $decoded ) ) {
578 return null;
579 }
580
581 return $decoded->refresh_jti ?? null;
582 }
583 }
584