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 +56 -23 27.928.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,8 +16,9 @@
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 22 use Yoast\WP\SEO\MyYoast_Client\Domain\Resource_Indicator;
23 23 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
24 24 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Encoding\Base64url;
@@ -73,15 +73,23 @@
73 73 */
74 74 private $expiring_store;
75 75
76 76 /**
77 + * The redirect URI provider port.
78 + *
79 + * @var Redirect_URI_Provider_Interface
80 + */
81 + private $redirect_uri_provider;
82 +
83 + /**
77 84 * Authorization_Code_Handler constructor.
78 85 *
79 - * @param Discovery_Interface $discovery The discovery port.
80 - * @param Client_Registration_Interface $client_registration The client registration port.
81 - * @param OAuth_Grant_Handler $grant_handler The OAuth grant handler.
82 - * @param ID_Token_Validator_Interface $id_token_validator The ID token validator port.
83 - * @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.
84 92 */
85 93 public function __construct(
86 94 Discovery_Interface $discovery,
87 95 Client_Registration_Interface $client_registration,
@@ -86,16 +94,18 @@
86 94 Discovery_Interface $discovery,
87 95 Client_Registration_Interface $client_registration,
88 96 OAuth_Grant_Handler $grant_handler,
89 97 ID_Token_Validator_Interface $id_token_validator,
90 - Expiring_Store $expiring_store
98 + Expiring_Store $expiring_store,
99 + Redirect_URI_Provider_Interface $redirect_uri_provider
91 100 ) {
92 - $this->discovery = $discovery;
93 - $this->client_registration = $client_registration;
94 - $this->grant_handler = $grant_handler;
95 - $this->id_token_validator = $id_token_validator;
96 - $this->expiring_store = $expiring_store;
97 - $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();
98 108 }
99 109
100 110 /**
101 111 * Builds the authorization URL for the user to visit.
@@ -102,9 +112,8 @@
102 112 *
103 113 * Generates PKCE challenge, state, and nonce, and stores them in the expiring store.
104 114 *
105 115 * @param int $user_id The WordPress user ID.
106 - * @param string $redirect_uri The callback redirect URI.
107 116 * @param string[] $scopes The scopes to request.
108 117 * @param Resource_Indicator $resource_indicator The RFC 8707 resource indicator the issued token should be bound to.
109 118 * @param string|null $return_url The URL to return the user to after authorization completes.
110 119 *
@@ -111,20 +120,28 @@
111 120 * @return string The authorization URL to redirect the user to.
112 121 *
113 122 * @throws Authorization_Flow_Exception If any of the auth flow prerequisites (registration, discovery, random number generation, or state parameter validation) fails.
114 123 */
115 - public function get_authorization_url( int $user_id, string $redirect_uri, array $scopes, Resource_Indicator $resource_indicator, ?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 {
116 125 if ( $user_id <= 0 ) {
117 126 throw new Authorization_Flow_Exception( 'invalid_user', 'A valid WordPress user ID is required to start the authorization flow.' );
118 127 }
119 128
120 - try {
121 - $registered_client = $this->client_registration->ensure_registered( [ $redirect_uri ] );
122 - } catch ( Registration_Failed_Exception $e ) {
123 - // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
124 - 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.' );
125 133 }
126 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 +
127 144 try {
128 145 $auth_endpoint = $this->discovery->get_document()->get_authorization_endpoint();
129 146 } catch ( Discovery_Failed_Exception | Server_Capability_Exception $e ) {
130 147 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
@@ -194,9 +211,9 @@
194 211 * @param string $state The state parameter from the callback.
195 212 *
196 213 * @return Token_Set The obtained tokens.
197 214 *
198 - * @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.
199 216 */
200 217 public function exchange_code( int $user_id, string $code, string $state ): Token_Set {
201 218 if ( $user_id <= 0 ) {
202 219 throw new Token_Request_Failed_Exception( 'invalid_user', 'A valid WordPress user ID is required to exchange an authorization code.' );
@@ -206,14 +223,14 @@
206 223
207 224 // Validate state (CSRF protection).
208 225 if ( ! \hash_equals( $flow_state->get_state(), $state ) ) {
209 226 $this->logger->warning( 'Authorization code exchange failed: state parameter mismatch for user {user_id} (potential CSRF).', [ 'user_id' => $user_id ] );
210 - $this->expiring_store->delete_for_user( self::CURRENT_AUTH_FLOW_STATE_KEY, $user_id );
227 + $this->discard_flow_state( $user_id );
211 228 throw new Token_Request_Failed_Exception( 'invalid_request', 'State parameter mismatch.' );
212 229 }
213 230
214 231 // Clean up the stored flow state.
215 - $this->expiring_store->delete_for_user( self::CURRENT_AUTH_FLOW_STATE_KEY, $user_id );
232 + $this->discard_flow_state( $user_id );
216 233
217 234 $resource_indicator = $flow_state->get_resource_indicator();
218 235 $grant = new Authorization_Code_Grant( $code, $flow_state->get_redirect_uri(), $flow_state->get_code_verifier() );
219 236 $token_set = $this->grant_handler->request_token( $grant, $resource_indicator );
@@ -220,8 +237,10 @@
220 237
221 238 // Validate ID token nonce (replay protection) if an ID token was returned.
222 239 $this->validate_id_token_nonce( $token_set, $flow_state );
223 240
241 + $this->client_registration->mark_uri_validated( $flow_state->get_redirect_uri() );
242 +
224 243 return $token_set;
225 244 }
226 245
227 246 /**
@@ -236,8 +255,22 @@
236 255 return $this->get_flow_state( $user_id )->get_return_url();
237 256 } catch ( Token_Request_Failed_Exception $e ) {
238 257 return null;
239 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 );
240 273 }
241 274
242 275 /**
243 276 * Validates the nonce claim in the ID token against the stored nonce.