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

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