PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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 / config / wincher-pkce-provider.php

wincher-pkce-provider.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at src/config/wincher-pkce-provider.php

258 lines 7.0 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\Config;
4
5 use Exception;
6 use UnexpectedValueException;
7 use YoastSEO_Vendor\GuzzleHttp\Exception\BadResponseException;
8 use YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException;
9 use YoastSEO_Vendor\League\OAuth2\Client\Provider\GenericProvider;
10 use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessToken;
11 use YoastSEO_Vendor\League\OAuth2\Client\Token\AccessTokenInterface;
12 use YoastSEO_Vendor\League\OAuth2\Client\Tool\BearerAuthorizationTrait;
13 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
14 use YoastSEO_Vendor\Psr\Log\InvalidArgumentException;
15
16 /**
17 * Class Wincher_PKCE_Provider
18 *
19 * @codeCoverageIgnore Ignoring as this class is purely a temporary wrapper until https://github.com/thephpleague/oauth2-client/pull/901 is merged.
20 *
21 * @phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase -- This class extends an external class.
22 * @phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- This class extends an external class.
23 */
24 class Wincher_PKCE_Provider extends GenericProvider {
25
26 use BearerAuthorizationTrait;
27
28 /**
29 * The method to use.
30 *
31 * @var string|null
32 */
33 protected $pkceMethod = null;
34
35 /**
36 * The PKCE code.
37 *
38 * @var string
39 */
40 protected $pkceCode;
41
42 /**
43 * Set the value of the pkceCode parameter.
44 *
45 * When using PKCE this should be set before requesting an access token.
46 *
47 * @param string $pkce_code The value for the pkceCode.
48 * @return self
49 */
50 public function setPkceCode( $pkce_code ) {
51 $this->pkceCode = $pkce_code;
52 return $this;
53 }
54
55 /**
56 * Returns the current value of the pkceCode parameter.
57 *
58 * This can be accessed by the redirect handler during authorization.
59 *
60 * @return string
61 */
62 public function getPkceCode() {
63 return $this->pkceCode;
64 }
65
66 /**
67 * Returns a new random string to use as PKCE code_verifier and
68 * hashed as code_challenge parameters in an authorization flow.
69 * Must be between 43 and 128 characters long.
70 *
71 * @param int $length Length of the random string to be generated.
72 *
73 * @return string
74 *
75 * @throws Exception Throws exception if an invalid value is passed to random_bytes.
76 */
77 protected function getRandomPkceCode( $length = 64 ) {
78 return \substr(
79 \strtr(
80 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
81 \base64_encode( \random_bytes( $length ) ),
82 '+/',
83 '-_',
84 ),
85 0,
86 $length,
87 );
88 }
89
90 /**
91 * Returns the current value of the pkceMethod parameter.
92 *
93 * @return string|null
94 */
95 protected function getPkceMethod() {
96 return $this->pkceMethod;
97 }
98
99 /**
100 * Returns authorization parameters based on provided options.
101 *
102 * @param array $options The options to use in the authorization parameters.
103 *
104 * @return array The authorization parameters
105 *
106 * @throws InvalidArgumentException Throws exception if an invalid PCKE method is passed in the options.
107 * @throws Exception When something goes wrong with generating the PKCE code.
108 */
109 protected function getAuthorizationParameters( array $options ) {
110 if ( empty( $options['state'] ) ) {
111 $options['state'] = $this->getRandomState();
112 }
113
114 if ( empty( $options['scope'] ) ) {
115 $options['scope'] = $this->getDefaultScopes();
116 }
117
118 $options += [
119 'response_type' => 'code',
120 ];
121
122 if ( \is_array( $options['scope'] ) ) {
123 $separator = $this->getScopeSeparator();
124 $options['scope'] = \implode( $separator, $options['scope'] );
125 }
126
127 // Store the state as it may need to be accessed later on.
128 $this->state = $options['state'];
129
130 $pkce_method = $this->getPkceMethod();
131 if ( ! empty( $pkce_method ) ) {
132 $this->pkceCode = $this->getRandomPkceCode();
133 if ( $pkce_method === 'S256' ) {
134 $options['code_challenge'] = \trim(
135 \strtr(
136 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
137 \base64_encode( \hash( 'sha256', $this->pkceCode, true ) ),
138 '+/',
139 '-_',
140 ),
141 '=',
142 );
143 }
144 elseif ( $pkce_method === 'plain' ) {
145 $options['code_challenge'] = $this->pkceCode;
146 }
147 else {
148 throw new InvalidArgumentException( 'Unknown PKCE method "' . $pkce_method . '".' );
149 }
150 $options['code_challenge_method'] = $pkce_method;
151 }
152
153 // Business code layer might set a different redirect_uri parameter.
154 // Depending on the context, leave it as-is.
155 if ( ! isset( $options['redirect_uri'] ) ) {
156 $options['redirect_uri'] = $this->redirectUri;
157 }
158
159 $options['client_id'] = $this->clientId;
160
161 return $options;
162 }
163
164 /**
165 * Requests an access token using a specified grant and option set.
166 *
167 * @param mixed $grant The grant to request access for.
168 * @param array $options The options to use with the current request.
169 *
170 * @return AccessToken|AccessTokenInterface The access token.
171 *
172 * @throws UnexpectedValueException Exception thrown if the provider response contains errors.
173 */
174 public function getAccessToken( $grant, array $options = [] ) {
175 $grant = $this->verifyGrant( $grant );
176
177 $params = [
178 'client_id' => $this->clientId,
179 'client_secret' => $this->clientSecret,
180 'redirect_uri' => $this->redirectUri,
181 ];
182
183 if ( ! empty( $this->pkceCode ) ) {
184 $params['code_verifier'] = $this->pkceCode;
185 }
186
187 $params = $grant->prepareRequestParameters( $params, $options );
188 $request = $this->getAccessTokenRequest( $params );
189 $response = $this->getParsedResponse( $request );
190
191 if ( \is_array( $response ) === false ) {
192 throw new UnexpectedValueException(
193 'Invalid response received from Authorization Server. Expected JSON.',
194 );
195 }
196
197 $prepared = $this->prepareAccessTokenResponse( $response );
198 $token = $this->createAccessToken( $prepared, $grant );
199
200 return $token;
201 }
202
203 /**
204 * Returns all options that can be configured.
205 *
206 * @return array The configurable options.
207 */
208 protected function getConfigurableOptions() {
209 return \array_merge(
210 $this->getRequiredOptions(),
211 [
212 'accessTokenMethod',
213 'accessTokenResourceOwnerId',
214 'scopeSeparator',
215 'responseError',
216 'responseCode',
217 'responseResourceOwnerId',
218 'scopes',
219 'pkceMethod',
220 ],
221 );
222 }
223
224 /**
225 * Parses the request response.
226 *
227 * @param RequestInterface $request The request interface.
228 *
229 * @return array The parsed response.
230 *
231 * @throws IdentityProviderException Exception thrown if there is no proper identity provider.
232 */
233 public function getParsedResponse( RequestInterface $request ) {
234 try {
235 $response = $this->getResponse( $request );
236 } catch ( BadResponseException $e ) {
237 $response = $e->getResponse();
238 }
239
240 $parsed = $this->parseResponse( $response );
241
242 $this->checkResponse( $response, $parsed );
243
244 // We always expect an array from the API except for on DELETE requests.
245 // We convert to an array here to prevent problems with array_key_exists on PHP8.
246 if ( ! \is_array( $parsed ) ) {
247 $parsed = [ 'data' => [] ];
248 }
249
250 // Add the response code as this is omitted from Winchers API.
251 if ( ! \array_key_exists( 'status', $parsed ) ) {
252 $parsed['status'] = $response->getStatusCode();
253 }
254
255 return $parsed;
256 }
257 }
258