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
← All changes | src/ai/authorization/application/token-manager.php +48 -37 27.5 → trunk View file →
@@ -9,9 +9,8 @@
9 9 use WPSEO_Utils;
10 10 use Yoast\WP\SEO\AI\Authorization\Infrastructure\Access_Token_User_Meta_Repository_Interface;
11 11 use Yoast\WP\SEO\AI\Authorization\Infrastructure\Code_Verifier_User_Meta_Repository;
12 12 use Yoast\WP\SEO\AI\Authorization\Infrastructure\Refresh_Token_User_Meta_Repository_Interface;
13 -use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
14 13 use Yoast\WP\SEO\AI\Generator\Infrastructure\WordPress_URLs;
15 14 use Yoast\WP\SEO\AI\HTTP_Request\Application\Request_Handler;
16 15 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
17 16 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
@@ -47,15 +46,8 @@
47 46 */
48 47 private $code_verifier;
49 48
50 49 /**
51 - * The consent handler.
52 - *
53 - * @var Consent_Handler
54 - */
55 - private $consent_handler;
56 -
57 - /**
58 50 * The refresh token repository.
59 51 *
60 52 * @var Refresh_Token_User_Meta_Repository_Interface
61 53 */
@@ -93,9 +85,8 @@
93 85 * Token_Manager constructor.
94 86 *
95 87 * @param Access_Token_User_Meta_Repository_Interface $access_token_repository The access token repository.
96 88 * @param Code_Verifier_Handler $code_verifier The code verifier service.
97 - * @param Consent_Handler $consent_handler The consent handler.
98 89 * @param Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository The refresh token repository.
99 90 * @param User_Helper $user_helper The user helper.
100 91 * @param Request_Handler $request_handler The request handler.
101 92 * @param Code_Verifier_User_Meta_Repository $code_verifier_repository The code verifier repository.
@@ -103,9 +94,8 @@
103 94 */
104 95 public function __construct(
105 96 Access_Token_User_Meta_Repository_Interface $access_token_repository,
106 97 Code_Verifier_Handler $code_verifier,
107 - Consent_Handler $consent_handler,
108 98 Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository,
109 99 User_Helper $user_helper,
110 100 Request_Handler $request_handler,
111 101 Code_Verifier_User_Meta_Repository $code_verifier_repository,
@@ -112,9 +102,8 @@
112 102 WordPress_URLs $urls
113 103 ) {
114 104 $this->access_token_repository = $access_token_repository;
115 105 $this->code_verifier = $code_verifier;
116 - $this->consent_handler = $consent_handler;
117 106 $this->refresh_token_repository = $refresh_token_repository;
118 107 $this->user_helper = $user_helper;
119 108 $this->request_handler = $request_handler;
120 109 $this->code_verifier_repository = $code_verifier_repository;
@@ -125,10 +114,13 @@
125 114
126 115 /**
127 116 * Invalidates the access token.
128 117 *
129 - * @param string $user_id The user ID.
118 + * The locally stored JWTs are always cleared, even when the remote invalidation fails — the
119 + * remote exception still propagates to the caller, but no credentials are left behind.
130 120 *
121 + * @param int $user_id The user ID.
122 + *
131 123 * @return void
132 124 *
133 125 * @throws Bad_Request_Exception Bad_Request_Exception.
134 126 * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
@@ -138,9 +130,9 @@
138 130 * @throws Service_Unavailable_Exception Service_Unavailable_Exception.
139 131 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
140 132 * @throws RuntimeException Unable to retrieve the access token.
141 133 */
142 - public function token_invalidate( string $user_id ): void {
134 + public function token_invalidate( int $user_id ): void {
143 135 try {
144 136 $access_jwt = $this->access_token_repository->get_token( $user_id );
145 137 } catch ( RuntimeException $e ) {
146 138 $access_jwt = '';
@@ -145,33 +137,68 @@
145 137 } catch ( RuntimeException $e ) {
146 138 $access_jwt = '';
147 139 }
148 140
149 - $request_body = [
150 - 'user_id' => (string) $user_id,
151 - ];
152 141 $request_headers = [
153 142 'Authorization' => "Bearer $access_jwt",
154 143 ];
155 144
156 145 try {
146 + // The endpoint takes no request body; the user is identified by the access token.
157 147 $this->request_handler->handle(
158 148 new Request(
159 149 '/token/invalidate',
160 - $request_body,
150 + [],
161 151 $request_headers,
162 152 ),
163 153 );
164 154 } catch ( Unauthorized_Exception | Forbidden_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose.
165 155 // If the credentials in our request were already invalid, our job is done and we continue to remove the tokens client-side.
156 + } finally {
157 + // Always clear the local tokens, even when the remote invalidation fails with an exception
158 + // that propagates: leaving credentials behind would contradict the intent of invalidating.
159 + $this->clear_tokens( $user_id );
166 160 }
161 + }
167 162
168 - // Delete the stored JWT tokens.
169 - $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_access_jwt' );
170 - $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_refresh_jwt' );
163 + /**
164 + * Clears the user meta tokens for a specific user.
165 + *
166 + * @param int $user_id The user id to delete this for.
167 + *
168 + * @return void
169 + */
170 + public function clear_tokens( int $user_id ): void {
171 + $this->access_token_repository->delete_token( $user_id );
172 + $this->refresh_token_repository->delete_token( $user_id );
171 173 }
172 174
173 175 /**
176 + * Checks whether any JWT (access or refresh) is stored locally for the user.
177 + *
178 + * @param int $user_id The user ID.
179 + *
180 + * @return bool Whether a locally stored JWT exists.
181 + */
182 + public function has_local_tokens( int $user_id ): bool {
183 + try {
184 + $this->access_token_repository->get_token( $user_id );
185 +
186 + return true;
187 + } catch ( RuntimeException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose.
188 + // No access token; fall through to the refresh token check.
189 + }
190 +
191 + try {
192 + $this->refresh_token_repository->get_token( $user_id );
193 +
194 + return true;
195 + } catch ( RuntimeException $e ) {
196 + return false;
197 + }
198 + }
199 +
200 + /**
174 201 * Requests a new set of JWT tokens.
175 202 *
176 203 * Requests a new JWT access and refresh token for a user from the Yoast AI Service and stores it in the database
177 204 * under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
@@ -190,17 +217,8 @@
190 217 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
191 218 * @throws Unauthorized_Exception Unauthorized_Exception.
192 219 */
193 220 public function token_request( WP_User $user ): void {
194 - // Ensure the user has given consent.
195 - if ( $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_consent', true ) !== '1' ) {
196 - // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
197 - $this->consent_handler->revoke_consent( $user->ID );
198 - throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 );
199 -
200 - // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
201 - }
202 -
203 221 // Generate a code verifier and store it in the database.
204 222 $code_verifier = $this->code_verifier->generate( $user->user_email );
205 223 $this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() );
206 224
@@ -315,10 +333,9 @@
315 333 */
316 334 public function get_or_request_access_token( WP_User $user ): string {
317 335 // If the site URL has changed since callback URLs were registered, delete stale tokens.
318 336 if ( $this->have_callback_urls_changed( $user ) ) {
319 - $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
320 - $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
337 + $this->clear_tokens( $user->ID );
321 338 }
322 339
323 340 $access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true );
324 341 if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
@@ -329,14 +346,8 @@
329 346 try {
330 347 $this->token_refresh( $user );
331 348 } catch ( Unauthorized_Exception $exception ) {
332 349 $this->token_request( $user );
333 - } catch ( Forbidden_Exception $exception ) {
334 - // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
335 - // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
336 - $this->consent_handler->revoke_consent( $user->ID );
337 - throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 );
338 - // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
339 350 }
340 351 $access_jwt = $this->access_token_repository->get_token( $user->ID );
341 352 }
342 353