PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.6
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.6
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 18.6, at src/config/wincher-pkce-provider.php

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