← All changes
|
src/myyoast-client/infrastructure/registration/client-registration.php
+227
-43
27.8
→
trunk
View file →
| @@ -6,12 +6,16 @@ | ||
| 6 | 6 | use InvalidArgumentException; |
| 7 | 7 | use Yoast\WP\SEO\Exceptions\Locking\Lock_Timeout_Exception; |
| 8 | 8 | use Yoast\WP\SEO\Helpers\Lock_Helper; |
| 9 | 9 | use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Discovery_Failed_Exception; |
| 10 | +use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Rate_Limited_Exception; | |
| 10 | 11 | use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Registration_Failed_Exception; |
| 12 | +use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Registration_Not_Found_Exception; | |
| 13 | +use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Registration_Temporarily_Unavailable_Exception; | |
| 11 | 14 | use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Server_Capability_Exception; |
| 12 | 15 | use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Client_Registration_Interface; |
| 13 | 16 | use Yoast\WP\SEO\MyYoast_Client\Domain\Auth_Token_Type; |
| 17 | +use Yoast\WP\SEO\MyYoast_Client\Domain\HTTP_Response; | |
| 14 | 18 | use Yoast\WP\SEO\MyYoast_Client\Domain\Registered_Client; |
| 15 | 19 | use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption; |
| 16 | 20 | use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption_Exception; |
| 17 | 21 | use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Key_Pair_Manager; |
| @@ -174,8 +178,9 @@ | ||
| 174 | 178 | $stored['client_id'], |
| 175 | 179 | $rat, |
| 176 | 180 | ( $stored['registration_client_uri'] ?? '' ), |
| 177 | 181 | ( $stored['metadata'] ?? [] ), |
| 182 | + ( $stored['validated_uris'] ?? [] ), | |
| 178 | 183 | ); |
| 179 | 184 | } catch ( InvalidArgumentException $e ) { |
| 180 | 185 | $this->logger->error( 'Stored registration data is invalid, clearing registration: {error}', [ 'error' => $e->getMessage() ] ); |
| 181 | 186 | $this->forget_registration(); |
| @@ -185,49 +190,25 @@ | ||
| 185 | 190 | return $this->cached_registered_clients[ $option_key ]; |
| 186 | 191 | } |
| 187 | 192 | |
| 188 | 193 | /** |
| 189 | - * Whether the plugin is registered as an OAuth client. | |
| 194 | + * Ensures the registration's redirect URIs exactly match the given set. | |
| 190 | 195 | * |
| 191 | - * When redirect URIs are provided, also verifies that all of them | |
| 192 | - * are included in the stored registration. | |
| 196 | + * Performs DCR when not yet registered; when registered with a different set, updates the | |
| 197 | + * registration in place via RFC 7592 (preserving the client_id, RAT, and key pair, and the | |
| 198 | + * verification state of unchanged URIs) rather than re-registering. | |
| 193 | 199 | * |
| 194 | - * @param string[] $redirect_uris Optional redirect URIs to verify against the stored registration. | |
| 200 | + * @param string[] $redirect_uris The exact set of OAuth redirect URIs the registration should have. | |
| 195 | 201 | * |
| 196 | - * @return bool | |
| 197 | - */ | |
| 198 | - public function is_registered( array $redirect_uris = [] ): bool { | |
| 199 | - $registered_client = $this->get_registered_client(); | |
| 200 | - if ( $registered_client === null ) { | |
| 201 | - return false; | |
| 202 | - } | |
| 203 | - | |
| 204 | - if ( $redirect_uris === [] ) { | |
| 205 | - return true; | |
| 206 | - } | |
| 207 | - | |
| 208 | - $stored_uris = ( $registered_client->get_metadata()['redirect_uris'] ?? [] ); | |
| 209 | - | |
| 210 | - return \array_diff( $redirect_uris, $stored_uris ) === []; | |
| 211 | - } | |
| 212 | - | |
| 213 | - /** | |
| 214 | - * Ensures the plugin is registered, performing DCR if needed. | |
| 215 | - * | |
| 216 | - * @param string[] $redirect_uris The OAuth redirect URIs to register with. | |
| 217 | - * | |
| 218 | 202 | * @return Registered_Client The client credentials. |
| 219 | 203 | * |
| 220 | 204 | * @throws Registration_Failed_Exception If registration fails. |
| 221 | 205 | */ |
| 222 | - public function ensure_registered( array $redirect_uris = [] ): Registered_Client { | |
| 223 | - if ( $this->is_registered( $redirect_uris ) ) { | |
| 224 | - return $this->get_registered_client(); | |
| 225 | - } | |
| 206 | + public function ensure_registered( array $redirect_uris ): Registered_Client { | |
| 207 | + $registered_client = $this->get_registered_client(); | |
| 226 | 208 | |
| 227 | - // Registered with stale redirect URIs — deregister first. | |
| 228 | - if ( $this->get_registered_client() !== null ) { | |
| 229 | - $this->deregister(); | |
| 209 | + if ( $registered_client !== null && $registered_client->has_redirect_uris( $redirect_uris ) ) { | |
| 210 | + return $registered_client; | |
| 230 | 211 | } |
| 231 | 212 | |
| 232 | 213 | if ( $redirect_uris === [] ) { |
| 233 | 214 | throw new Registration_Failed_Exception( 'At least one redirect URI is required for initial registration.' ); |
| @@ -232,8 +213,14 @@ | ||
| 232 | 213 | if ( $redirect_uris === [] ) { |
| 233 | 214 | throw new Registration_Failed_Exception( 'At least one redirect URI is required for initial registration.' ); |
| 234 | 215 | } |
| 235 | 216 | |
| 217 | + // Registered, but the redirect-URI set differs — update in place (RFC 7592 PUT) so the | |
| 218 | + // client_id survives and unchanged URIs keep their verification state and tokens. | |
| 219 | + if ( $registered_client !== null ) { | |
| 220 | + return $this->update_redirect_uris( $redirect_uris ); | |
| 221 | + } | |
| 222 | + | |
| 236 | 223 | return $this->register( $redirect_uris ); |
| 237 | 224 | } |
| 238 | 225 | |
| 239 | 226 | /** |
| @@ -240,9 +227,11 @@ | ||
| 240 | 227 | * Reads the current client registration from the server (RFC 7592 GET). |
| 241 | 228 | * |
| 242 | 229 | * @return array<string, string|string[]> The registration metadata. |
| 243 | 230 | * |
| 244 | - * @throws Registration_Failed_Exception If the read fails. | |
| 231 | + * @throws Registration_Not_Found_Exception If the server reports the registration is gone (HTTP 401/404). | |
| 232 | + * @throws Rate_Limited_Exception If the server rate-limited the request (HTTP 429). | |
| 233 | + * @throws Registration_Failed_Exception If the read fails for any other reason. | |
| 245 | 234 | */ |
| 246 | 235 | public function read_registration(): array { |
| 247 | 236 | $registered_client = $this->get_registered_client(); |
| 248 | 237 | if ( $registered_client === null ) { |
| @@ -268,14 +257,20 @@ | ||
| 268 | 257 | |
| 269 | 258 | if ( $result->get_status() === 401 || $result->get_status() === 404 ) { |
| 270 | 259 | $this->logger->warning( 'Registration is no longer valid (HTTP {status}), clearing local registration.', [ 'status' => $result->get_status() ] ); |
| 271 | 260 | $this->forget_registration(); |
| 272 | - throw new Registration_Failed_Exception( | |
| 261 | + throw new Registration_Not_Found_Exception( | |
| 273 | 262 | // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. |
| 274 | 263 | 'Registration is no longer valid (HTTP ' . $result->get_status() . ').', |
| 275 | 264 | ); |
| 276 | 265 | } |
| 277 | 266 | |
| 267 | + if ( $result->get_status() === 429 ) { | |
| 268 | + $this->logger->warning( 'Registration read was rate-limited (HTTP 429).' ); | |
| 269 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. | |
| 270 | + throw new Rate_Limited_Exception( 'Registration read was rate-limited (HTTP 429).', $this->get_retry_after_seconds( $result ) ); | |
| 271 | + } | |
| 272 | + | |
| 278 | 273 | if ( ! $result->is_successful() ) { |
| 279 | 274 | $error_message = (string) $result->get_body_value( 'error_description', $result->get_body_value( 'error', '' ) ); |
| 280 | 275 | throw new Registration_Failed_Exception( |
| 281 | 276 | // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. |
| @@ -283,12 +278,18 @@ | ||
| 283 | 278 | ); |
| 284 | 279 | } |
| 285 | 280 | |
| 286 | 281 | $body = $result->get_body(); |
| 287 | - if ( ! \is_array( $body ) ) { | |
| 282 | + if ( ! \is_array( $body ) || empty( $body['client_id'] ) ) { | |
| 288 | 283 | throw new Registration_Failed_Exception( 'Invalid response from registration endpoint.' ); |
| 289 | 284 | } |
| 290 | 285 | |
| 286 | + // The server is authoritative: heal local data that has drifted from it (for example when a | |
| 287 | + // site migration rewrote the stored redirect URIs directly in the database, bypassing the | |
| 288 | + // registration round-trip). The GET body carries no RAT, so store_credentials preserves the | |
| 289 | + // stored one. | |
| 290 | + $this->store_credentials( $body ); | |
| 291 | + | |
| 291 | 292 | return $body; |
| 292 | 293 | } |
| 293 | 294 | |
| 294 | 295 | /** |
| @@ -355,8 +356,85 @@ | ||
| 355 | 356 | return $this->store_credentials( $body ); |
| 356 | 357 | } |
| 357 | 358 | |
| 358 | 359 | /** |
| 360 | + * Updates the registered redirect URIs in place (RFC 7592 PUT). | |
| 361 | + * | |
| 362 | + * Preserves the client_id, registration access token, and key pair. Verification state for | |
| 363 | + * URIs that remain in the set is preserved; URIs no longer present are dropped. | |
| 364 | + * | |
| 365 | + * @param string[] $redirect_uris The new exact set of redirect URIs. | |
| 366 | + * | |
| 367 | + * @return Registered_Client The updated credentials. | |
| 368 | + * | |
| 369 | + * @throws Registration_Not_Found_Exception If the server reports the registration is gone (HTTP 401/404). | |
| 370 | + * @throws Rate_Limited_Exception If the server rate-limited the request (HTTP 429). | |
| 371 | + * @throws Registration_Failed_Exception If the update fails for any other reason. | |
| 372 | + */ | |
| 373 | + private function update_redirect_uris( array $redirect_uris ): Registered_Client { | |
| 374 | + $registered_client = $this->get_registered_client(); | |
| 375 | + if ( $registered_client === null ) { | |
| 376 | + throw new Registration_Failed_Exception( 'Not registered.' ); | |
| 377 | + } | |
| 378 | + | |
| 379 | + // Per RFC 7592 §2.2, server-assigned fields MUST NOT be included; keep the existing key pair. | |
| 380 | + $request_body = $this->build_update_request_body( $registered_client->get_metadata() ); | |
| 381 | + $request_body['redirect_uris'] = \array_values( $redirect_uris ); | |
| 382 | + $request_body['software_statement'] = $this->issuer_config->get_software_statement(); | |
| 383 | + | |
| 384 | + // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- Encoding for HTTP request body, not user-facing output. | |
| 385 | + $json = \wp_json_encode( $request_body ); | |
| 386 | + if ( $json === false ) { | |
| 387 | + throw new Registration_Failed_Exception( 'Failed to JSON-encode registration request body.' ); | |
| 388 | + } | |
| 389 | + | |
| 390 | + $result = $this->http_client->authenticated_request( | |
| 391 | + 'PUT', | |
| 392 | + $registered_client->get_registration_client_uri(), | |
| 393 | + $registered_client->get_registration_access_token(), | |
| 394 | + Auth_Token_Type::BEARER, | |
| 395 | + [ | |
| 396 | + 'headers' => [ | |
| 397 | + 'Content-Type' => 'application/json', | |
| 398 | + 'Accept' => 'application/json', | |
| 399 | + ], | |
| 400 | + 'body' => $json, | |
| 401 | + 'timeout' => 15, | |
| 402 | + ], | |
| 403 | + ); | |
| 404 | + | |
| 405 | + if ( $result->get_status() === 401 || $result->get_status() === 404 ) { | |
| 406 | + $this->logger->warning( 'Registration is no longer valid on update (HTTP {status}), clearing local registration.', [ 'status' => $result->get_status() ] ); | |
| 407 | + $this->forget_registration(); | |
| 408 | + throw new Registration_Not_Found_Exception( | |
| 409 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. | |
| 410 | + 'Registration is no longer valid (HTTP ' . $result->get_status() . ').', | |
| 411 | + ); | |
| 412 | + } | |
| 413 | + | |
| 414 | + if ( $result->get_status() === 429 ) { | |
| 415 | + $this->logger->warning( 'Registration update was rate-limited (HTTP 429).' ); | |
| 416 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. | |
| 417 | + throw new Rate_Limited_Exception( 'Registration update was rate-limited (HTTP 429).', $this->get_retry_after_seconds( $result ) ); | |
| 418 | + } | |
| 419 | + | |
| 420 | + if ( ! $result->is_successful() ) { | |
| 421 | + $error_message = (string) $result->get_body_value( 'error_description', $result->get_body_value( 'error', '' ) ); | |
| 422 | + throw new Registration_Failed_Exception( | |
| 423 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. | |
| 424 | + \sprintf( 'Redirect URI update returned HTTP %d: %s', $result->get_status(), $error_message ), | |
| 425 | + ); | |
| 426 | + } | |
| 427 | + | |
| 428 | + $body = $result->get_body(); | |
| 429 | + if ( ! \is_array( $body ) || empty( $body['client_id'] ) ) { | |
| 430 | + throw new Registration_Failed_Exception( 'Redirect URI update returned invalid response.' ); | |
| 431 | + } | |
| 432 | + | |
| 433 | + return $this->store_credentials( $body ); | |
| 434 | + } | |
| 435 | + | |
| 436 | + /** | |
| 359 | 437 | * Deletes the client registration from the server (RFC 7592 DELETE) and clears local data. |
| 360 | 438 | * |
| 361 | 439 | * @return bool True if deleted or already not registered, false on network failure. |
| 362 | 440 | */ |
| @@ -413,25 +491,101 @@ | ||
| 413 | 491 | $this->key_pair_manager->rotate_key_pair( Key_Pair_Manager::PURPOSE_DPOP ); |
| 414 | 492 | } |
| 415 | 493 | |
| 416 | 494 | /** |
| 495 | + * Whether the given redirect URI has completed the OAuth authorization-code flow on this site. | |
| 496 | + * | |
| 497 | + * The state lives on the stored registration: it is pruned to the current redirect-URI set | |
| 498 | + * whenever those change, and invalidated when the client is deregistered. | |
| 499 | + * | |
| 500 | + * @param string $redirect_uri The redirect URI to check. | |
| 501 | + * | |
| 502 | + * @return bool | |
| 503 | + */ | |
| 504 | + public function is_uri_validated( string $redirect_uri ): bool { | |
| 505 | + $registered_client = $this->get_registered_client(); | |
| 506 | + | |
| 507 | + return $registered_client !== null && $registered_client->is_uri_validated( $redirect_uri ); | |
| 508 | + } | |
| 509 | + | |
| 510 | + /** | |
| 511 | + * Records that the given redirect URI has completed the authorization-code flow. | |
| 512 | + * | |
| 513 | + * No-op when the site is not registered or the URI was already recorded. Idempotent: | |
| 514 | + * `update_option()` short-circuits when the stored value is unchanged. | |
| 515 | + * | |
| 516 | + * @param string $redirect_uri The redirect URI that completed the auth-code flow. | |
| 517 | + * | |
| 518 | + * @return void | |
| 519 | + */ | |
| 520 | + public function mark_uri_validated( string $redirect_uri ): void { | |
| 521 | + $registered_client = $this->get_registered_client(); | |
| 522 | + if ( $registered_client === null ) { | |
| 523 | + return; | |
| 524 | + } | |
| 525 | + | |
| 526 | + $validated_uris = $registered_client->get_validated_uris(); | |
| 527 | + if ( \in_array( $redirect_uri, $validated_uris, true ) ) { | |
| 528 | + return; | |
| 529 | + } | |
| 530 | + | |
| 531 | + $validated_uris[] = $redirect_uri; | |
| 532 | + | |
| 533 | + $option_key = $this->get_option_key(); | |
| 534 | + $stored = \get_option( $option_key, [] ); | |
| 535 | + if ( \is_array( $stored ) ) { | |
| 536 | + $stored['validated_uris'] = $validated_uris; | |
| 537 | + \update_option( $option_key, $stored, false ); | |
| 538 | + } | |
| 539 | + | |
| 540 | + $this->cached_registered_clients[ $option_key ] = $registered_client->with_validated_uris( $validated_uris ); | |
| 541 | + } | |
| 542 | + | |
| 543 | + /** | |
| 417 | 544 | * Stores the DCR response credentials securely. |
| 418 | 545 | * |
| 546 | + * A registration read (RFC 7592 GET) response carries no registration access token; when the | |
| 547 | + * body omits the RAT, the existing stored RAT is preserved rather than overwritten with an | |
| 548 | + * empty value. | |
| 549 | + * | |
| 419 | 550 | * @param array<string, string|array<string>> $response_body The parsed DCR response body. |
| 420 | 551 | * |
| 421 | 552 | * @return Registered_Client The stored credentials. |
| 422 | 553 | */ |
| 423 | 554 | private function store_credentials( array $response_body ): Registered_Client { |
| 424 | - $option_key = $this->get_option_key(); | |
| 425 | - $encrypted_rat = $this->encryption->encrypt( | |
| 426 | - ( $response_body['registration_access_token'] ?? '' ), | |
| 427 | - self::ENCRYPTION_CONTEXT, | |
| 428 | - ); | |
| 555 | + $option_key = $this->get_option_key(); | |
| 556 | + $existing = $this->get_registered_client(); | |
| 429 | 557 | |
| 558 | + // The RFC 7592 GET response never re-sends the RAT, so a missing key means "keep the stored | |
| 559 | + // one" — encrypting the absent value would brick every future management call. Only a body | |
| 560 | + // that explicitly carries a RAT (DCR / PUT) replaces it. | |
| 561 | + if ( \array_key_exists( 'registration_access_token', $response_body ) ) { | |
| 562 | + $rat = $response_body['registration_access_token']; | |
| 563 | + $encrypted_rat = $this->encryption->encrypt( $rat, self::ENCRYPTION_CONTEXT ); | |
| 564 | + } | |
| 565 | + else { | |
| 566 | + // Reuse the already-decrypted RAT and its stored ciphertext rather than re-encrypting. | |
| 567 | + $rat = ( $existing !== null ) ? $existing->get_registration_access_token() : ''; | |
| 568 | + $stored = \get_option( $option_key, [] ); | |
| 569 | + $encrypted_rat = ( \is_array( $stored ) ) ? ( $stored['encrypted_rat'] ?? '' ) : ''; | |
| 570 | + } | |
| 571 | + | |
| 430 | 572 | // Strip the RAT from metadata — it is stored encrypted separately. |
| 431 | 573 | $metadata = $response_body; |
| 432 | 574 | unset( $metadata['registration_access_token'] ); |
| 433 | 575 | |
| 576 | + // Preserve validation state across an in-place update or key rotation (same client_id), but | |
| 577 | + // reset it for a fresh registration: a new client_id means the redirect URIs must be | |
| 578 | + // re-validated from scratch. Always prune to the new redirect-URI set so a removed URI loses | |
| 579 | + // its verification and an added one starts unverified. | |
| 580 | + $validated_uris = []; | |
| 581 | + if ( $existing !== null && $existing->get_client_id() === $response_body['client_id'] ) { | |
| 582 | + $new_redirect_uris = ( $metadata['redirect_uris'] ?? [] ); | |
| 583 | + if ( \is_array( $new_redirect_uris ) ) { | |
| 584 | + $validated_uris = \array_values( \array_intersect( $existing->get_validated_uris(), $new_redirect_uris ) ); | |
| 585 | + } | |
| 586 | + } | |
| 587 | + | |
| 434 | 588 | \update_option( |
| 435 | 589 | $option_key, |
| 436 | 590 | [ |
| 437 | 591 | 'client_id' => $response_body['client_id'], |
| @@ -437,8 +591,9 @@ | ||
| 437 | 591 | 'client_id' => $response_body['client_id'], |
| 438 | 592 | 'encrypted_rat' => $encrypted_rat, |
| 439 | 593 | 'registration_client_uri' => ( $response_body['registration_client_uri'] ?? '' ), |
| 440 | 594 | 'metadata' => $metadata, |
| 595 | + 'validated_uris' => $validated_uris, | |
| 441 | 596 | ], |
| 442 | 597 | false, |
| 443 | 598 | ); |
| 444 | 599 | |
| @@ -443,11 +598,12 @@ | ||
| 443 | 598 | ); |
| 444 | 599 | |
| 445 | 600 | $this->cached_registered_clients[ $option_key ] = new Registered_Client( |
| 446 | 601 | $response_body['client_id'], |
| 447 | - ( $response_body['registration_access_token'] ?? '' ), | |
| 602 | + $rat, | |
| 448 | 603 | ( $response_body['registration_client_uri'] ?? '' ), |
| 449 | 604 | $metadata, |
| 605 | + $validated_uris, | |
| 450 | 606 | ); |
| 451 | 607 | |
| 452 | 608 | return $this->cached_registered_clients[ $option_key ]; |
| 453 | 609 | } |
| @@ -461,8 +617,20 @@ | ||
| 461 | 617 | return self::OPTION_KEY_PREFIX . $this->issuer_config->get_issuer_key(); |
| 462 | 618 | } |
| 463 | 619 | |
| 464 | 620 | /** |
| 621 | + * Extracts the `Retry-After` value (in seconds) from a 429 response, if any. | |
| 622 | + * | |
| 623 | + * @param HTTP_Response $result The 429 response. | |
| 624 | + * | |
| 625 | + * @return int|null Seconds until retry, or null when absent or unparseable. | |
| 626 | + */ | |
| 627 | + private function get_retry_after_seconds( HTTP_Response $result ): ?int { | |
| 628 | + $headers = $result->get_headers(); | |
| 629 | + return Rate_Limited_Exception::parse_retry_after( ( $headers['retry-after'] ?? null ) ); | |
| 630 | + } | |
| 631 | + | |
| 632 | + /** | |
| 465 | 633 | * Performs the actual DCR registration request. |
| 466 | 634 | * |
| 467 | 635 | * @param string[] $redirect_uris The OAuth redirect URIs to register. |
| 468 | 636 | * |
| @@ -467,9 +635,11 @@ | ||
| 467 | 635 | * @param string[] $redirect_uris The OAuth redirect URIs to register. |
| 468 | 636 | * |
| 469 | 637 | * @return Registered_Client The registration result. |
| 470 | 638 | * |
| 471 | - * @throws Registration_Failed_Exception If registration fails. | |
| 639 | + * @throws Registration_Temporarily_Unavailable_Exception If the server temporarily refuses new registrations (HTTP 503). | |
| 640 | + * @throws Rate_Limited_Exception If the server rate-limited the request (HTTP 429). | |
| 641 | + * @throws Registration_Failed_Exception If registration fails for any other reason. | |
| 472 | 642 | */ |
| 473 | 643 | private function do_register( array $redirect_uris ): Registered_Client { |
| 474 | 644 | try { |
| 475 | 645 | $registration_endpoint = $this->discovery_client->get_document()->get_registration_endpoint(); |
| @@ -521,8 +691,22 @@ | ||
| 521 | 691 | if ( $result->is_transport_failure() ) { |
| 522 | 692 | $error_message = (string) $result->get_body_value( 'error_description', '' ); |
| 523 | 693 | // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. |
| 524 | 694 | throw new Registration_Failed_Exception( 'DCR request failed: ' . $error_message ); |
| 695 | + } | |
| 696 | + | |
| 697 | + // The server temporarily refuses new registrations (rollout brake engaged). | |
| 698 | + // Surface it as a typed transient failure carrying the (display-only) retry hint. | |
| 699 | + if ( $result->get_status() === 503 && $result->get_body_value( 'error' ) === 'temporarily_unavailable' ) { | |
| 700 | + $error_message = (string) $result->get_body_value( 'error_description', 'Client registration is temporarily disabled.' ); | |
| 701 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. | |
| 702 | + throw new Registration_Temporarily_Unavailable_Exception( $error_message, $this->get_retry_after_seconds( $result ) ); | |
| 703 | + } | |
| 704 | + | |
| 705 | + if ( $result->get_status() === 429 ) { | |
| 706 | + $this->logger->warning( 'DCR was rate-limited (HTTP 429).' ); | |
| 707 | + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. | |
| 708 | + throw new Rate_Limited_Exception( 'DCR was rate-limited (HTTP 429).', $this->get_retry_after_seconds( $result ) ); | |
| 525 | 709 | } |
| 526 | 710 | |
| 527 | 711 | if ( $result->get_status() !== 201 ) { |
| 528 | 712 | $error_message = (string) $result->get_body_value( 'error_description', $result->get_body_value( 'error', '' ) ); |