PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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 / domain / discovery-document.php

discovery-document.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at src/myyoast-client/domain/discovery-document.php

264 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\MyYoast_Client\Domain;
4
5 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Discovery_Failed_Exception;
6 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Server_Capability_Exception;
7
8 /**
9 * Immutable value object representing an OIDC discovery document.
10 *
11 * Wraps the full response from `{issuer}/.well-known/openid-configuration`.
12 * The constructor validates spec-required fields (OIDC Discovery 1.0 Section 3),
13 * client-required fields (registration and revocation endpoints), and server
14 * capabilities (grant types, PKCE, DPoP, etc.).
15 */
16 class Discovery_Document {
17
18 /**
19 * String fields required by the OIDC Discovery 1.0 specification (Section 3).
20 *
21 * Note: `token_endpoint` is spec-required unless only the Implicit Flow is used;
22 * since this client uses Authorization Code + PKCE, it is always required here.
23 *
24 * @var string[]
25 */
26 private const SPEC_REQUIRED_STRING_FIELDS = [
27 'issuer',
28 'authorization_endpoint',
29 'token_endpoint',
30 'jwks_uri',
31 ];
32
33 /**
34 * Array fields required by the OIDC Discovery 1.0 specification (Section 3).
35 *
36 * @var string[]
37 */
38 private const SPEC_REQUIRED_ARRAY_FIELDS = [
39 'response_types_supported',
40 'subject_types_supported',
41 'id_token_signing_alg_values_supported',
42 ];
43
44 /**
45 * String fields not required by the OIDC spec but required by this client.
46 *
47 * - `registration_endpoint`: RECOMMENDED per OIDC Discovery spec; required for DCR (RFC 7591).
48 * - `revocation_endpoint`: Defined by RFC 8414; required for token revocation (RFC 7009).
49 *
50 * @var string[]
51 */
52 private const CLIENT_REQUIRED_STRING_FIELDS = [
53 'registration_endpoint',
54 'revocation_endpoint',
55 ];
56
57 /**
58 * The full discovery document data.
59 *
60 * @var array<string, string|string[]|bool>
61 */
62 private $data;
63
64 /**
65 * Discovery_Document constructor.
66 *
67 * Validates that all OIDC-spec-required fields are present and that the
68 * server advertises support for all features this client depends on.
69 *
70 * @param array<string, string|string[]|bool> $data The full discovery document data.
71 *
72 * @throws Discovery_Failed_Exception If spec-required fields are missing or empty.
73 * @throws Server_Capability_Exception If the server lacks required capabilities.
74 *
75 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- Server_Capability_Exception is thrown by validate_server_capabilities().
76 */
77 public function __construct( array $data ) {
78 $spec_invalid = [];
79 foreach ( self::SPEC_REQUIRED_STRING_FIELDS as $key ) {
80 if ( ! isset( $data[ $key ] ) || ! \is_string( $data[ $key ] ) || $data[ $key ] === '' ) {
81 $spec_invalid[] = $key;
82 }
83 }
84 foreach ( self::SPEC_REQUIRED_ARRAY_FIELDS as $key ) {
85 if ( ! isset( $data[ $key ] ) || ! self::is_non_empty_string_array( $data[ $key ] ) ) {
86 $spec_invalid[] = $key;
87 }
88 }
89
90 if ( ! empty( $spec_invalid ) ) {
91 throw new Discovery_Failed_Exception(
92 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
93 \sprintf( 'OIDC discovery document has missing or invalid spec-required fields: %s', \implode( ', ', $spec_invalid ) ),
94 );
95 }
96
97 $client_invalid = [];
98 foreach ( self::CLIENT_REQUIRED_STRING_FIELDS as $key ) {
99 if ( ! isset( $data[ $key ] ) || ! \is_string( $data[ $key ] ) || $data[ $key ] === '' ) {
100 $client_invalid[] = $key;
101 }
102 }
103
104 if ( ! empty( $client_invalid ) ) {
105 throw new Server_Capability_Exception(
106 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
107 \sprintf( 'Server does not provide endpoints required by this client: %s', \implode( ', ', $client_invalid ) ),
108 );
109 }
110
111 self::validate_server_capabilities( $data );
112
113 $this->data = $data;
114 }
115
116 /**
117 * Returns the issuer identifier.
118 *
119 * @return string
120 */
121 public function get_issuer(): string {
122 return $this->data['issuer'];
123 }
124
125 /**
126 * Returns the authorization endpoint URL.
127 *
128 * @return string
129 */
130 public function get_authorization_endpoint(): string {
131 return $this->data['authorization_endpoint'];
132 }
133
134 /**
135 * Returns the token endpoint URL.
136 *
137 * @return string
138 */
139 public function get_token_endpoint(): string {
140 return $this->data['token_endpoint'];
141 }
142
143 /**
144 * Returns the dynamic client registration endpoint URL.
145 *
146 * @return string
147 */
148 public function get_registration_endpoint(): string {
149 return $this->data['registration_endpoint'];
150 }
151
152 /**
153 * Returns the token revocation endpoint URL.
154 *
155 * @return string
156 */
157 public function get_revocation_endpoint(): string {
158 return $this->data['revocation_endpoint'];
159 }
160
161 /**
162 * Returns the JSON Web Key Set URI.
163 *
164 * @return string
165 */
166 public function get_jwks_uri(): string {
167 return $this->data['jwks_uri'];
168 }
169
170 /**
171 * Returns the full discovery document data for cache storage.
172 *
173 * Stores the complete server response so that newly required fields
174 * are available from cache without requiring a fresh fetch.
175 *
176 * @return array<string, string|string[]|bool> The full discovery document.
177 */
178 public function to_array(): array {
179 return $this->data;
180 }
181
182 /**
183 * Validates that the server advertises support for all required features.
184 *
185 * @param array<string, string|string[]|bool> $config The parsed discovery document.
186 *
187 * @return void
188 *
189 * @throws Server_Capability_Exception If the server lacks required capabilities.
190 */
191 private static function validate_server_capabilities( array $config ): void {
192 $checks = [
193 [
194 'field' => 'code_challenge_methods_supported',
195 'required' => 'S256',
196 'message' => 'Server does not support S256 PKCE code challenge method.',
197 ],
198 [
199 'field' => 'grant_types_supported',
200 'required' => 'authorization_code',
201 'message' => 'Server does not support authorization_code grant type.',
202 ],
203 [
204 'field' => 'grant_types_supported',
205 'required' => 'refresh_token',
206 'message' => 'Server does not support refresh_token grant type.',
207 ],
208 [
209 'field' => 'grant_types_supported',
210 'required' => 'client_credentials',
211 'message' => 'Server does not support client_credentials grant type.',
212 ],
213 [
214 'field' => 'token_endpoint_auth_methods_supported',
215 'required' => 'private_key_jwt',
216 'message' => 'Server does not support private_key_jwt authentication.',
217 ],
218 [
219 'field' => 'token_endpoint_auth_signing_alg_values_supported',
220 'required' => 'EdDSA',
221 'message' => 'Server does not support EdDSA for token endpoint auth signing.',
222 ],
223 [
224 'field' => 'dpop_signing_alg_values_supported',
225 'required' => 'EdDSA',
226 'message' => 'Server does not support EdDSA for DPoP signing.',
227 ],
228 ];
229
230 foreach ( $checks as $check ) {
231 $supported = ( $config[ $check['field'] ] ?? [] );
232 if ( ! \is_array( $supported ) || ! \in_array( $check['required'], $supported, true ) ) {
233 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
234 throw new Server_Capability_Exception( $check['message'] );
235 }
236 }
237 }
238
239 /**
240 * Checks whether a value is a non-empty array containing only strings.
241 *
242 * phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint -- Validation method, accepts any type for checking.
243 *
244 * @param mixed $value The value to check.
245 *
246 * phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint
247 *
248 * @return bool True if the value is a non-empty array of strings.
249 */
250 private static function is_non_empty_string_array( $value ): bool {
251 if ( ! \is_array( $value ) || $value === [] ) {
252 return false;
253 }
254
255 foreach ( $value as $item ) {
256 if ( ! \is_string( $item ) ) {
257 return false;
258 }
259 }
260
261 return true;
262 }
263 }
264