PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / trunk
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF vtrunk
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / vendor / wp-media / mcp-oauth / inc / Auth / ClaudeClientVerifier.php

ClaudeClientVerifier.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF trunk, at vendor/wp-media/mcp-oauth/inc/Auth/ClaudeClientVerifier.php

148 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Built-in verification for trusted CIMD publishers.
4 *
5 * Client ID Metadata Documents (CIMD) let any client identify itself with an
6 * HTTPS URL, but this proof of concept only authorizes clients whose metadata
7 * matches a known, trusted publisher. Claude is the bundled publisher: its
8 * official client_id URL, host, and loopback redirect shape are pinned here so
9 * that a fetched document can be cryptographically bound to Claude rather than
10 * trusted on its own say-so.
11 */
12
13 declare(strict_types=1);
14
15 namespace WPMedia\MCP\OAuth\Auth;
16
17 use WPMedia\MCP\OAuth\Logging\McpLogger;
18
19 class ClaudeClientVerifier {
20 /**
21 * Verify a fetched CIMD document against the trusted-publisher allowlist.
22 *
23 * @param string $client_id The client_id URL the document was fetched from.
24 * @param array<string, mixed> $doc The decoded, already URL-validated metadata document.
25 * @return array{verified: bool, publisher: string} Verification result.
26 */
27 public function verify( string $client_id, array $doc ): array {
28 $unverified = [
29 'verified' => false,
30 'publisher' => '',
31 ];
32
33 foreach ( $this->get_trusted_publishers() as $publisher => $config ) {
34 if ( $this->matches_publisher( $client_id, $doc, (array) $config ) ) {
35 McpLogger::log(
36 'CIMD',
37 'client verified against trusted publisher',
38 [
39 'client_id' => $client_id,
40 'publisher' => $publisher,
41 ]
42 );
43
44 return [
45 'verified' => true,
46 'publisher' => (string) $publisher,
47 ];
48 }
49 }
50
51 return $unverified;
52 }
53
54 /**
55 * Whether a client_id URL's host belongs to a trusted publisher.
56 *
57 * Used to gate the (unauthenticated) CIMD network fetch: only client_ids
58 * whose host is allowlisted are ever dereferenced, so an arbitrary URL
59 * cannot turn the public authorize endpoint into an outbound-request proxy
60 * or fill the transient cache with junk records.
61 *
62 * @param string $client_id The client_id URL.
63 * @return bool
64 */
65 public function is_trusted_host( string $client_id ): bool {
66 $host = (string) wp_parse_url( $client_id, PHP_URL_HOST );
67 if ( '' === $host ) {
68 return false;
69 }
70
71 foreach ( $this->get_trusted_publishers() as $config ) {
72 if ( (string) ( ( (array) $config )['host'] ?? '' ) === $host ) {
73 return true;
74 }
75 }
76
77 return false;
78 }
79
80 /**
81 * Return the trusted-publisher allowlist.
82 *
83 * @return array<string, array<string, mixed>>
84 */
85 private function get_trusted_publishers(): array {
86 $publishers = [
87 'claude' => [
88 'client_ids' => [
89 'https://claude.ai/oauth/claude-code-client-metadata',
90 'https://claude.ai/oauth/mcp-oauth-client-metadata',
91 ],
92 'host' => 'claude.ai',
93 ],
94 ];
95
96 $publishers = apply_filters_deprecated( 'rocket_mcp_trusted_publishers', [ $publishers ], '1.0.1', 'wpmedia_mcp_oauth_trusted_publishers' );
97
98 /**
99 * Filters the trusted-publisher allowlist for MCP OAuth client verification.
100 *
101 * Each entry is keyed by an arbitrary publisher slug and must provide:
102 * - client_ids (string[]): exact client_id URLs that are trusted for this publisher.
103 * - host (string): the hostname client_id URLs must resolve to (defense in depth,
104 * also used as the SSRF allowlist gate before any network fetch is made).
105 *
106 * This filter only ever runs server-side, with no request-derived input passed into it
107 * or influencing its evaluation — it does not accept or process untrusted input. It can
108 * only ADD trusted publishers; it does not bypass the exact client_id match in
109 * matches_publisher() or the "verified" hard-reject in AuthorizeEndpoint::handle_request().
110 *
111 * @param array<string, array{client_ids: string[], host: string}> $publishers Trusted-publisher allowlist.
112 * @return array<string, array{client_ids: string[], host: string}>
113 */
114 return wpm_apply_filters_typed( 'array', 'wpmedia_mcp_oauth_trusted_publishers', $publishers );
115 }
116
117 /**
118 * Check whether a document matches a single trusted publisher.
119 *
120 * @param string $client_id The client_id URL.
121 * @param array<string, mixed> $doc The metadata document.
122 * @param array<string, mixed> $config The publisher configuration.
123 * @return bool
124 */
125 private function matches_publisher( string $client_id, array $doc, array $config ): bool {
126 $client_ids = (array) ( $config['client_ids'] ?? [] );
127
128 // 1. Exact, known-good client_id URL (primary trust anchor).
129 if ( ! in_array( $client_id, $client_ids, true ) ) {
130 return false;
131 }
132
133 // 2. URL host must match the publisher host (defense in depth).
134 $host = (string) wp_parse_url( $client_id, PHP_URL_HOST );
135 if ( '' === $host || (string) ( $config['host'] ?? '' ) !== $host ) {
136 return false;
137 }
138
139 // 3. Public-client flow only.
140 // Redirect URI validation at authorize time is AuthorizeEndpoint::redirect_uri_matches()'s
141 // responsibility. Checking the redirect_uri shape here would break verification when
142 // the publisher's CIMD document includes additional redirect URIs for other products.
143 $auth_method = (string) ( $doc['token_endpoint_auth_method'] ?? 'none' );
144
145 return 'none' === $auth_method;
146 }
147 }
148