🏠 Root
/
home
/
a
/
r
/
t
/
artorgp
/
www
/
wp-content
/
plugins
/
wordpress-seo
/
src
/
myyoast-client
/
infrastructure
/
oidc
/
Editing: discovery-client.php
<?php // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC; use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Discovery_Failed_Exception; use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Server_Capability_Exception; use Yoast\WP\SEO\MyYoast_Client\Application\Ports\Discovery_Interface; use Yoast\WP\SEO\MyYoast_Client\Domain\Discovery_Document; use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Http\HTTP_Client; use YoastSEO_Vendor\Psr\Log\LoggerAwareInterface; use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait; use YoastSEO_Vendor\Psr\Log\NullLogger; /** * Fetches and caches the OpenID Connect discovery document. * * Discovers all endpoint URLs dynamically from `{issuer}/.well-known/openid-configuration`. * Caches the document as a WordPress transient for 24 hours. */ class Discovery_Client implements Discovery_Interface, LoggerAwareInterface { use LoggerAwareTrait; private const CACHE_TRANSIENT_PREFIX = 'wpseo_myyoast_oidc_'; private const CACHE_TTL = \DAY_IN_SECONDS; /** * The issuer configuration. * * @var Issuer_Config */ private $issuer_config; /** * The HTTP client. * * @var HTTP_Client */ private $http_client; /** * In-memory cache of the discovery document. * * @var Discovery_Document|null */ private $cached_document = null; /** * Discovery_Client constructor. * * @param Issuer_Config $issuer_config The issuer configuration. * @param HTTP_Client $http_client The HTTP client. */ public function __construct( Issuer_Config $issuer_config, HTTP_Client $http_client ) { $this->issuer_config = $issuer_config; $this->http_client = $http_client; $this->logger = new NullLogger(); } /** * Returns the validated discovery document. * * @return Discovery_Document The validated discovery document. * * @throws Discovery_Failed_Exception If the document cannot be fetched. * @throws Server_Capability_Exception If the server lacks required capabilities. */ public function get_document(): Discovery_Document { // Check in-memory cache: invalidate if issuer has changed. if ( $this->cached_document !== null ) { if ( $this->cached_document->get_issuer() === $this->issuer_config->get_issuer_url() ) { return $this->cached_document; } $this->cached_document = null; } $cached = \get_transient( $this->get_cache_key() ); if ( \is_array( $cached ) && ! empty( $cached ) ) { try { $this->cached_document = new Discovery_Document( $cached ); return $this->cached_document; } catch ( Discovery_Failed_Exception | Server_Capability_Exception $e ) { // Cached data is corrupted or no longer compatible — fetch fresh. $this->logger->info( 'Invalidating cached discovery document: {error}', [ 'error' => $e->getMessage() ] ); \delete_transient( $this->get_cache_key() ); } } return $this->fetch_and_cache(); } /** * Invalidates the cached discovery document. * * @return void */ public function invalidate_cache(): void { $this->cached_document = null; \delete_transient( $this->get_cache_key() ); } /** * Returns the issuer-specific transient cache key. * * The issuer URL is hashed into the key so that switching issuers * (e.g. via environment variable or filter) naturally causes a cache miss * instead of returning stale data from a different server. * * @return string The transient key. */ private function get_cache_key(): string { return self::CACHE_TRANSIENT_PREFIX . $this->issuer_config->get_issuer_key(); } /** * Fetches the discovery document from the server, validates it, and caches it. * * @return Discovery_Document The validated discovery document. * * @throws Discovery_Failed_Exception If the document cannot be fetched or parsed. */ private function fetch_and_cache(): Discovery_Document { $url = $this->issuer_config->get_discovery_url(); $result = $this->http_client->request( 'GET', $url, [ 'timeout' => 10, 'headers' => [ 'Accept' => 'application/json' ], ], ); if ( $result->is_transport_failure() ) { $error_message = (string) $result->get_body_value( 'error_description', '' ); throw new Discovery_Failed_Exception( // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. \sprintf( 'Failed to fetch OIDC discovery document from %s: %s', $url, $error_message ), ); } if ( $result->get_status() !== 200 ) { throw new Discovery_Failed_Exception( // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. \sprintf( 'OIDC discovery returned HTTP %d from %s.', $result->get_status(), $url ), ); } $body = $result->get_body(); if ( ! \is_array( $body ) ) { throw new Discovery_Failed_Exception( 'OIDC discovery returned invalid JSON.' ); } $document = new Discovery_Document( $body ); // OIDC Discovery 1.0 Section 4.1: the issuer in the document must match the expected issuer. $expected_issuer = $this->issuer_config->get_issuer_url(); if ( $document->get_issuer() !== $expected_issuer ) { throw new Discovery_Failed_Exception( // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message. \sprintf( 'Issuer mismatch: expected %s, got %s.', $expected_issuer, $document->get_issuer() ), ); } \set_transient( $this->get_cache_key(), $body, self::CACHE_TTL ); $this->cached_document = $document; return $document; } }
Save
Cancel