PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
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
← All changes | src/myyoast-client/application/authorization-code-handler.php +69 -29 27.828.5 View file →
@@ -10,9 +10,8 @@
10 10 use Yoast\WP\SEO\Expiring_Store\Domain\Key_Not_Found_Exception;
11 11 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Authorization_Flow_Exception;
12 12 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Discovery_Failed_Exception;
13 13 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\ID_Token_Validation_Exception;
14 -use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Registration_Failed_Exception;
15 14 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Server_Capability_Exception;
16 15 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Token_Request_Failed_Exception;
17 16 use Yoast\WP\SEO\MyYoast_Client\Application\Grants\Authorization_Code_Grant;
18 17 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface;
@@ -17,9 +16,11 @@
17 16 use Yoast\WP\SEO\MyYoast_Client\Application\Grants\Authorization_Code_Grant;
18 17 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface;
19 18 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Discovery_Interface;
20 19 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\ID_Token_Validator_Interface;
20 +use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Redirect_URI_Provider_Interface;
21 21 use Yoast\WP\SEO\MyYoast_Client\Domain\Auth_Flow_State;
22 +use Yoast\WP\SEO\MyYoast_Client\Domain\Resource_Indicator;
22 23 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
23 24 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Encoding\Base64url;
24 25 use YoastSEO_Vendor\Psr\Log\LoggerAwareInterface;
25 26 use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait;
@@ -72,15 +73,23 @@
72 73 */
73 74 private $expiring_store;
74 75
75 76 /**
77 + * The redirect URI provider port.
78 + *
79 + * @var Redirect_URI_Provider_Interface
80 + */
81 + private $redirect_uri_provider;
82 +
83 + /**
76 84 * Authorization_Code_Handler constructor.
77 85 *
78 - * @param Discovery_Interface $discovery The discovery port.
79 - * @param Client_Registration_Interface $client_registration The client registration port.
80 - * @param OAuth_Grant_Handler $grant_handler The OAuth grant handler.
81 - * @param ID_Token_Validator_Interface $id_token_validator The ID token validator port.
82 - * @param Expiring_Store $expiring_store The expiring store.
86 + * @param Discovery_Interface $discovery The discovery port.
87 + * @param Client_Registration_Interface $client_registration The client registration port.
88 + * @param OAuth_Grant_Handler $grant_handler The OAuth grant handler.
89 + * @param ID_Token_Validator_Interface $id_token_validator The ID token validator port.
90 + * @param Expiring_Store $expiring_store The expiring store.
91 + * @param Redirect_URI_Provider_Interface $redirect_uri_provider The redirect URI provider port.
83 92 */
84 93 public function __construct(
85 94 Discovery_Interface $discovery,
86 95 Client_Registration_Interface $client_registration,
@@ -85,16 +94,18 @@
85 94 Discovery_Interface $discovery,
86 95 Client_Registration_Interface $client_registration,
87 96 OAuth_Grant_Handler $grant_handler,
88 97 ID_Token_Validator_Interface $id_token_validator,
89 - Expiring_Store $expiring_store
98 + Expiring_Store $expiring_store,
99 + Redirect_URI_Provider_Interface $redirect_uri_provider
90 100 ) {
91 - $this->discovery = $discovery;
92 - $this->client_registration = $client_registration;
93 - $this->grant_handler = $grant_handler;
94 - $this->id_token_validator = $id_token_validator;
95 - $this->expiring_store = $expiring_store;
96 - $this->logger = new NullLogger();
101 + $this->discovery = $discovery;
102 + $this->client_registration = $client_registration;
103 + $this->grant_handler = $grant_handler;
104 + $this->id_token_validator = $id_token_validator;
105 + $this->expiring_store = $expiring_store;
106 + $this->redirect_uri_provider = $redirect_uri_provider;
107 + $this->logger = new NullLogger();
97 108 }
98 109
99 110 /**
100 111 * Builds the authorization URL for the user to visit.
@@ -100,29 +111,37 @@
100 111 * Builds the authorization URL for the user to visit.
101 112 *
102 113 * Generates PKCE challenge, state, and nonce, and stores them in the expiring store.
103 114 *
104 - * @param int $user_id The WordPress user ID.
105 - * @param string $redirect_uri The callback redirect URI.
106 - * @param string[] $scopes The scopes to request.
107 - * @param string|null $return_url The URL to return the user to after authorization completes.
115 + * @param int $user_id The WordPress user ID.
116 + * @param string[] $scopes The scopes to request.
117 + * @param Resource_Indicator $resource_indicator The RFC 8707 resource indicator the issued token should be bound to.
118 + * @param string|null $return_url The URL to return the user to after authorization completes.
108 119 *
109 120 * @return string The authorization URL to redirect the user to.
110 121 *
111 122 * @throws Authorization_Flow_Exception If any of the auth flow prerequisites (registration, discovery, random number generation, or state parameter validation) fails.
112 123 */
113 - public function get_authorization_url( int $user_id, string $redirect_uri, array $scopes = [], ?string $return_url = null ): string {
124 + public function get_authorization_url( int $user_id, array $scopes, Resource_Indicator $resource_indicator, ?string $return_url = null ): string {
114 125 if ( $user_id <= 0 ) {
115 126 throw new Authorization_Flow_Exception( 'invalid_user', 'A valid WordPress user ID is required to start the authorization flow.' );
116 127 }
117 128
118 - try {
119 - $registered_client = $this->client_registration->ensure_registered( [ $redirect_uri ] );
120 - } catch ( Registration_Failed_Exception $e ) {
121 - // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
122 - throw new Authorization_Flow_Exception( 'registration_failed', $e->getMessage(), 0, $e );
129 + // Registration is a prerequisite handled by the connect flow; this method never triggers DCR.
130 + $registered_client = $this->client_registration->get_registered_client();
131 + if ( $registered_client === null ) {
132 + throw new Authorization_Flow_Exception( 'not_registered', 'Site is not registered with MyYoast; complete the registration first.' );
123 133 }
124 134
135 + // Resolve which registered redirect URI to embed in this request (the server matches it exactly).
136 + $redirect_uri = $this->redirect_uri_provider->get_authorization_redirect_uri(
137 + $registered_client,
138 + $user_id,
139 + $scopes,
140 + $resource_indicator,
141 + $return_url,
142 + );
143 +
125 144 try {
126 145 $auth_endpoint = $this->discovery->get_document()->get_authorization_endpoint();
127 146 } catch ( Discovery_Failed_Exception | Server_Capability_Exception $e ) {
128 147 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
@@ -145,9 +164,9 @@
145 164 throw new Authorization_Flow_Exception( 'random_failure', 'Failed to generate secure random values.', 0, $e );
146 165 }
147 166
148 167 try {
149 - $flow_state = new Auth_Flow_State( $code_verifier, $state, $nonce, $redirect_uri, $return_url );
168 + $flow_state = new Auth_Flow_State( $code_verifier, $state, $nonce, $redirect_uri, $return_url, $resource_indicator );
150 169 } catch ( InvalidArgumentException $e ) {
151 170 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
152 171 throw new Authorization_Flow_Exception( 'invalid_state', $e->getMessage(), 0, $e );
153 172 }
@@ -173,8 +192,12 @@
173 192 if ( $nonce !== null ) {
174 193 $params['nonce'] = $nonce;
175 194 }
176 195
196 + if ( ! $resource_indicator->is_default() ) {
197 + $params['resource'] = $resource_indicator->value();
198 + }
199 +
177 200 return $auth_endpoint . '?' . \http_build_query( $params, '', '&', \PHP_QUERY_RFC3986 );
178 201 }
179 202
180 203 /**
@@ -188,9 +211,9 @@
188 211 * @param string $state The state parameter from the callback.
189 212 *
190 213 * @return Token_Set The obtained tokens.
191 214 *
192 - * @throws Registration_Failed_Exception|Token_Request_Failed_Exception If client registration or exchange fails.
215 + * @throws Token_Request_Failed_Exception If the site is not registered or the exchange fails.
193 216 */
194 217 public function exchange_code( int $user_id, string $code, string $state ): Token_Set {
195 218 if ( $user_id <= 0 ) {
196 219 throw new Token_Request_Failed_Exception( 'invalid_user', 'A valid WordPress user ID is required to exchange an authorization code.' );
@@ -200,21 +223,24 @@
200 223
201 224 // Validate state (CSRF protection).
202 225 if ( ! \hash_equals( $flow_state->get_state(), $state ) ) {
203 226 $this->logger->warning( 'Authorization code exchange failed: state parameter mismatch for user {user_id} (potential CSRF).', [ 'user_id' => $user_id ] );
204 - $this->expiring_store->delete_for_user( self::CURRENT_AUTH_FLOW_STATE_KEY, $user_id );
227 + $this->discard_flow_state( $user_id );
205 228 throw new Token_Request_Failed_Exception( 'invalid_request', 'State parameter mismatch.' );
206 229 }
207 230
208 231 // Clean up the stored flow state.
209 - $this->expiring_store->delete_for_user( self::CURRENT_AUTH_FLOW_STATE_KEY, $user_id );
232 + $this->discard_flow_state( $user_id );
210 233
211 - $grant = new Authorization_Code_Grant( $code, $flow_state->get_redirect_uri(), $flow_state->get_code_verifier() );
212 - $token_set = $this->grant_handler->request_token( $grant );
234 + $resource_indicator = $flow_state->get_resource_indicator();
235 + $grant = new Authorization_Code_Grant( $code, $flow_state->get_redirect_uri(), $flow_state->get_code_verifier() );
236 + $token_set = $this->grant_handler->request_token( $grant, $resource_indicator );
213 237
214 238 // Validate ID token nonce (replay protection) if an ID token was returned.
215 239 $this->validate_id_token_nonce( $token_set, $flow_state );
216 240
241 + $this->client_registration->mark_uri_validated( $flow_state->get_redirect_uri() );
242 +
217 243 return $token_set;
218 244 }
219 245
220 246 /**
@@ -229,8 +255,22 @@
229 255 return $this->get_flow_state( $user_id )->get_return_url();
230 256 } catch ( Token_Request_Failed_Exception $e ) {
231 257 return null;
232 258 }
259 + }
260 +
261 + /**
262 + * Discards any pending authorization-flow state for a user.
263 + *
264 + * Used when the provider returns an error (e.g. the user denied consent) so a
265 + * stale flow can't be resumed. A no-op when no flow is pending.
266 + *
267 + * @param int $user_id The WordPress user ID.
268 + *
269 + * @return void
270 + */
271 + public function discard_flow_state( int $user_id ): void {
272 + $this->expiring_store->delete_for_user( self::CURRENT_AUTH_FLOW_STATE_KEY, $user_id );
233 273 }
234 274
235 275 /**
236 276 * Validates the nonce claim in the ID token against the stored nonce.