PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.8
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.8
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / modules / Mcp / Mcp_Auth.php

Mcp_Auth.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.8, at includes/modules/Mcp/Mcp_Auth.php

114 lines 3.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP authentication — the permission_callback for MCP-originated REST
4 * calls proxied by the hosted broker (api.xspeedcache.com).
5 *
6 * The Free plugin's REST routes all gate on
7 * `current_user_can( 'manage_options' )` + a WP nonce
8 * (class-rest-api.php), and Rest_Manager::wrap_permission() forces that
9 * cap check as an always-on gate a module can only make stricter. MCP
10 * calls arrive server-to-server from the broker with NO logged-in user,
11 * so they can't use that path.
12 *
13 * Instead the broker presents the per-site `site_token` in the
14 * `X-XSpeed-MCP-Token` header on every proxied call. This class
15 * validates that token (constant-time) against the value stored by the
16 * pairing flow. Routes wired to Mcp_Auth::permission() are registered
17 * DIRECTLY by McpModule::boot() (not via Rest_Manager) precisely so they
18 * can bypass the forced cap check.
19 *
20 * Security contract:
21 * - The token is a 32-byte random secret minted at connect time.
22 * - Comparison is constant-time (hash_equals) to avoid timing oracles.
23 * - A missing/empty stored token means "not connected" → always deny.
24 * - This is an ADDITIONAL credential surface exposed only on Pro and
25 * only after an admin explicitly connects; it never weakens the
26 * existing admin-only routes.
27 *
28 * @package XSpeed
29 */
30
31 declare(strict_types=1);
32
33 namespace XSpeed\Modules\Mcp;
34
35 defined( 'ABSPATH' ) || exit;
36
37 final class Mcp_Auth {
38
39 /** Header the broker sends the site token in. */
40 public const TOKEN_HEADER = 'X-XSpeed-MCP-Token';
41
42 /**
43 * The permission_callback for MCP tool routes.
44 *
45 * Accepts the request iff a valid `site_token` is presented in the
46 * X-XSpeed-MCP-Token header. Does NOT fall through to
47 * current_user_can — these routes are token-only by design.
48 *
49 * @param \WP_REST_Request $request Incoming request.
50 * @return bool|\WP_Error True when authorized, WP_Error(401) otherwise.
51 */
52 public static function permission( \WP_REST_Request $request ) {
53 // Locked-out IPs are rejected before any token comparison.
54 if ( Mcp_Rate_Limiter::is_locked() ) {
55 return new \WP_Error(
56 'xspeed_mcp_rate_limited',
57 __( 'Too many failed attempts. Try again later.', 'xspeed' ),
58 array( 'status' => 429 )
59 );
60 }
61
62 $presented = self::extract_token( $request );
63
64 if ( '' === $presented ) {
65 Mcp_Rate_Limiter::record_failure();
66 return self::unauthorized( 'Missing MCP token.' );
67 }
68
69 $stored = Mcp_Pairing::site_token();
70
71 if ( '' === $stored ) {
72 // Not connected — nothing to authenticate against.
73 return self::unauthorized( 'This site is not connected to xSpeed MCP.' );
74 }
75
76 if ( ! hash_equals( $stored, $presented ) ) {
77 Mcp_Rate_Limiter::record_failure();
78 return self::unauthorized( 'Invalid MCP token.' );
79 }
80
81 Mcp_Rate_Limiter::clear();
82 return true;
83 }
84
85 /**
86 * Read the site token from the request header. Trimmed; empty string
87 * when absent.
88 *
89 * @param \WP_REST_Request $request Incoming request.
90 */
91 private static function extract_token( \WP_REST_Request $request ): string {
92 $header = $request->get_header( self::TOKEN_HEADER );
93 if ( ! is_string( $header ) ) {
94 return '';
95 }
96 return trim( $header );
97 }
98
99 /**
100 * Build a 401 WP_Error with the REST status attached so the broker
101 * (and any direct caller) sees a proper HTTP 401.
102 *
103 * @param string $message Human-readable reason.
104 * @return \WP_Error
105 */
106 private static function unauthorized( string $message ) {
107 return new \WP_Error(
108 'xspeed_mcp_unauthorized',
109 $message,
110 array( 'status' => 401 )
111 );
112 }
113 }
114