PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / myyoast-client / application / oauth-grant-handler.php

oauth-grant-handler.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at src/myyoast-client/application/oauth-grant-handler.php

187 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3
4 namespace Yoast\WP\SEO\MyYoast_Client\Application;
5
6 use InvalidArgumentException;
7 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Client_Authentication_Exception;
8 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Discovery_Failed_Exception;
9 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Server_Capability_Exception;
10 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Token_Request_Failed_Exception;
11 use Yoast\WP\SEO\MyYoast_Client\Application\Grants\Grant_Interface;
12 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Authenticator_Interface;
13 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface;
14 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Discovery_Interface;
15 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\OAuth_Server_Client_Interface;
16 use Yoast\WP\SEO\MyYoast_Client\Domain\Resource_Indicator;
17 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
18 use YoastSEO_Vendor\Psr\Log\LoggerAwareInterface;
19 use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait;
20 use YoastSEO_Vendor\Psr\Log\NullLogger;
21
22 /**
23 * Central handler for OAuth token endpoint requests.
24 *
25 * Handles client authentication (private_key_jwt) and delegates grant-specific
26 * parameters to the provided Grant_Interface. Executes the token request via
27 * the token endpoint client and handles error responses.
28 */
29 class OAuth_Grant_Handler implements LoggerAwareInterface {
30 use LoggerAwareTrait;
31
32 /**
33 * The discovery port.
34 *
35 * @var Discovery_Interface
36 */
37 private $discovery;
38
39 /**
40 * The client registration port.
41 *
42 * @var Client_Registration_Interface
43 */
44 private $client_registration;
45
46 /**
47 * The client authenticator port.
48 *
49 * @var Client_Authenticator_Interface
50 */
51 private $client_authenticator;
52
53 /**
54 * The token endpoint client port.
55 *
56 * @var OAuth_Server_Client_Interface
57 */
58 private $oauth_server_client;
59
60 /**
61 * OAuth_Grant_Handler constructor.
62 *
63 * @param Discovery_Interface $discovery The discovery port.
64 * @param Client_Registration_Interface $client_registration The client registration port.
65 * @param Client_Authenticator_Interface $client_authenticator The client authenticator port.
66 * @param OAuth_Server_Client_Interface $oauth_server_client The token endpoint client port.
67 */
68 public function __construct(
69 Discovery_Interface $discovery,
70 Client_Registration_Interface $client_registration,
71 Client_Authenticator_Interface $client_authenticator,
72 OAuth_Server_Client_Interface $oauth_server_client
73 ) {
74 $this->discovery = $discovery;
75 $this->client_registration = $client_registration;
76 $this->client_authenticator = $client_authenticator;
77 $this->oauth_server_client = $oauth_server_client;
78 $this->logger = new NullLogger();
79 }
80
81 /**
82 * Executes a token endpoint request using the provided grant strategy.
83 *
84 * Ensures the client is registered, creates a client assertion, merges
85 * grant-specific parameters, and sends the request. The resource indicator
86 * is added to the body (unless it's the default-resource instance) and
87 * stamped onto the resulting Token_Set so storage and audit code can
88 * introspect the audience.
89 *
90 * @param Grant_Interface $grant The grant strategy providing grant-specific parameters.
91 * @param Resource_Indicator $resource_indicator The resource indicator (RFC 8707) the grant targets. Use Resource_Indicator::default() for the default resource.
92 *
93 * @return Token_Set The token set from the response.
94 *
95 * @throws Token_Request_Failed_Exception If the token request fails.
96 */
97 public function request_token( Grant_Interface $grant, Resource_Indicator $resource_indicator ): Token_Set {
98 // A token request requires an existing registration; it never triggers DCR or a
99 // redirect-URI update — that is the connect flow's responsibility.
100 $registered_client = $this->client_registration->get_registered_client();
101 if ( $registered_client === null ) {
102 throw new Token_Request_Failed_Exception( 'not_registered', 'Site is not registered with MyYoast; complete the connect flow first.' );
103 }
104
105 try {
106 $token_endpoint = $this->discovery->get_document()->get_token_endpoint();
107 } catch ( Discovery_Failed_Exception |Server_Capability_Exception $e ) {
108 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
109 throw new Token_Request_Failed_Exception( 'discovery_failed', $e->getMessage(), 0, $e );
110 }
111
112 $client_id = $registered_client->get_client_id();
113
114 try {
115 $client_assertion = $this->client_authenticator->create_client_assertion( $client_id, $token_endpoint );
116 } catch ( Client_Authentication_Exception $e ) {
117 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
118 throw new Token_Request_Failed_Exception( 'client_authentication_failed', $e->getMessage(), 0, $e );
119 }
120
121 $body = \array_merge(
122 [
123 'grant_type' => $grant->get_grant_type(),
124 'client_id' => $client_id,
125 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
126 'client_assertion' => $client_assertion,
127 ],
128 $grant->get_grant_params(),
129 );
130
131 // RFC 8707 resource indicator is cross-cutting — independent of grant type.
132 // The Resource_Indicator value object proves the value is already canonical.
133 if ( ! $resource_indicator->is_default() ) {
134 $body['resource'] = $resource_indicator->value();
135 }
136
137 $result = $this->oauth_server_client->request(
138 'POST',
139 $token_endpoint,
140 [
141 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded' ],
142 'body' => $body,
143 'dpop' => true,
144 ],
145 );
146
147 if ( ! $result->is_successful() ) {
148 $error = (string) $result->get_body_value( 'error', 'unknown' );
149 $description = (string) $result->get_body_value( 'error_description', '' );
150 $this->logger->warning(
151 'Token request failed for grant {grant_type}: HTTP {status}, error={error} {description}',
152 [
153 'grant_type' => $grant->get_grant_type(),
154 'status' => $result->get_status(),
155 'error' => $error,
156 'description' => $description,
157 ],
158 );
159
160 $body = $result->get_body();
161 if ( \is_array( $body ) && isset( $body['error'] ) ) {
162 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
163 throw Token_Request_Failed_Exception::from_response( $body, $result->get_status() );
164 }
165 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
166 throw new Token_Request_Failed_Exception( 'token_request_failed', 'HTTP ' . $result->get_status(), $result->get_status() );
167 }
168
169 $body = $result->get_body();
170 if ( ! \is_array( $body ) ) {
171 throw new Token_Request_Failed_Exception( 'invalid_token_response', 'Token endpoint did not return a JSON object.' );
172 }
173
174 try {
175 $token_set = Token_Set::from_response( $body );
176 } catch ( InvalidArgumentException $e ) {
177 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
178 throw new Token_Request_Failed_Exception( 'invalid_token_response', $e->getMessage(), 0, $e );
179 }
180
181 // Per RFC 8707's trust model (§2, §4), the client is authoritative for the
182 // canonical resource indicator. We always stamp the requested value on the
183 // result rather than honouring any echoed `resource` field from the AS.
184 return $token_set->with_resource_indicator( $resource_indicator );
185 }
186 }
187