PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
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 1.9.13 1.9.12 1.9.11 1.9.10 All 159 releases
woocommerce-pos / includes / API / V1 / Auth.php

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

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