PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.9
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.9
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 27.9, at src/myyoast-client/application/oauth-grant-handler.php

182 lines 7.1 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 $registered_client = $this->client_registration->ensure_registered();
99
100 try {
101 $token_endpoint = $this->discovery->get_document()->get_token_endpoint();
102 } catch ( Discovery_Failed_Exception |Server_Capability_Exception $e ) {
103 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
104 throw new Token_Request_Failed_Exception( 'discovery_failed', $e->getMessage(), 0, $e );
105 }
106
107 $client_id = $registered_client->get_client_id();
108
109 try {
110 $client_assertion = $this->client_authenticator->create_client_assertion( $client_id, $token_endpoint );
111 } catch ( Client_Authentication_Exception $e ) {
112 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
113 throw new Token_Request_Failed_Exception( 'client_authentication_failed', $e->getMessage(), 0, $e );
114 }
115
116 $body = \array_merge(
117 [
118 'grant_type' => $grant->get_grant_type(),
119 'client_id' => $client_id,
120 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
121 'client_assertion' => $client_assertion,
122 ],
123 $grant->get_grant_params(),
124 );
125
126 // RFC 8707 resource indicator is cross-cutting — independent of grant type.
127 // The Resource_Indicator value object proves the value is already canonical.
128 if ( ! $resource_indicator->is_default() ) {
129 $body['resource'] = $resource_indicator->value();
130 }
131
132 $result = $this->oauth_server_client->request(
133 'POST',
134 $token_endpoint,
135 [
136 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded' ],
137 'body' => $body,
138 'dpop' => true,
139 ],
140 );
141
142 if ( ! $result->is_successful() ) {
143 $error = (string) $result->get_body_value( 'error', 'unknown' );
144 $description = (string) $result->get_body_value( 'error_description', '' );
145 $this->logger->warning(
146 'Token request failed for grant {grant_type}: HTTP {status}, error={error} {description}',
147 [
148 'grant_type' => $grant->get_grant_type(),
149 'status' => $result->get_status(),
150 'error' => $error,
151 'description' => $description,
152 ],
153 );
154
155 $body = $result->get_body();
156 if ( \is_array( $body ) && isset( $body['error'] ) ) {
157 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
158 throw Token_Request_Failed_Exception::from_response( $body, $result->get_status() );
159 }
160 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
161 throw new Token_Request_Failed_Exception( 'token_request_failed', 'HTTP ' . $result->get_status(), $result->get_status() );
162 }
163
164 $body = $result->get_body();
165 if ( ! \is_array( $body ) ) {
166 throw new Token_Request_Failed_Exception( 'invalid_token_response', 'Token endpoint did not return a JSON object.' );
167 }
168
169 try {
170 $token_set = Token_Set::from_response( $body );
171 } catch ( InvalidArgumentException $e ) {
172 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
173 throw new Token_Request_Failed_Exception( 'invalid_token_response', $e->getMessage(), 0, $e );
174 }
175
176 // Per RFC 8707's trust model (§2, §4), the client is authoritative for the
177 // canonical resource indicator. We always stamp the requested value on the
178 // result rather than honouring any echoed `resource` field from the AS.
179 return $token_set->with_resource_indicator( $resource_indicator );
180 }
181 }
182