PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.27
Booktics – Appointment Booking Calendar for Service Businesses v1.0.27
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
← All changes | core/module/google-calendar/Controller/calendar-auth-controller.php +21 -96 trunk → 1.0.27 View file →
@@ -40,18 +40,8 @@
40 40 */
41 41 const BGC_USER_META_TOKEN = 'booktics_google_auth_token';
42 42
43 43 /**
44 - * Transient prefix for one-time OAuth state tokens.
45 - */
46 - const BGC_OAUTH_STATE_PREFIX = 'booktics_gcal_oauth_';
47 -
48 - /**
49 - * Lifetime of an OAuth state token.
50 - */
51 - const BGC_OAUTH_STATE_TTL = 900; // 15 minutes.
52 -
53 - /**
54 44 * Google OAuth configuration.
55 45 *
56 46 * @var array
57 47 */
@@ -239,92 +229,31 @@
239 229 return $refreshed_token;
240 230 }
241 231
242 232 /**
243 - * Issue a one-time OAuth state token.
244 - *
245 - * The token is random and carries no caller-supplied data; the real
246 - * payload (who started the flow and which team member it is for) is held
247 - * server-side in a transient. This is what makes the callback trustworthy:
248 - * the state cannot be forged to name an arbitrary user, and it cannot be
249 - * replayed because it is deleted on first use.
250 - *
251 - * @param int $team_member_id Target team member/user id.
252 - * @return string The opaque state token to hand to Google.
253 - */
254 - private function issue_oauth_state( $team_member_id ) {
255 - $token = wp_generate_password( 40, false, false ); // URL-safe: letters + digits only.
256 -
257 - set_transient(
258 - self::BGC_OAUTH_STATE_PREFIX . $token,
259 - array(
260 - 'initiator' => get_current_user_id(),
261 - 'team_member_id' => absint( $team_member_id ),
262 - ),
263 - self::BGC_OAUTH_STATE_TTL
264 - );
265 -
266 - return $token;
267 - }
268 -
269 - /**
270 - * Validate and consume a one-time OAuth state token.
271 - *
272 - * @param string $token State token returned by Google.
273 - * @return array|false Stored { initiator, team_member_id }, or false when
274 - * the token is unknown, expired or already used.
275 - */
276 - private function consume_oauth_state( $token ) {
277 - $key = self::BGC_OAUTH_STATE_PREFIX . sanitize_text_field( $token );
278 - $data = get_transient( $key );
279 -
280 - if ( false === $data || ! is_array( $data ) || ! isset( $data['initiator'], $data['team_member_id'] ) ) {
281 - return false;
282 - }
283 -
284 - delete_transient( $key ); // Single use.
285 -
286 - return $data;
287 - }
288 -
289 - /**
290 233 * Handle direct OAuth callback from Google.
291 234 */
292 235 public function handle_google_auth_callback() {
293 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback; CSRF is prevented by the signed, single-use state verified below.
236 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
294 237 if ( ! isset( $_GET['code'] ) ) {
295 238 return;
296 239 }
297 240
298 - // A server-issued state is mandatory. It is a random, single-use token
299 - // bound to the initiating user and the target team member. Without a
300 - // valid state we cannot trust who this callback is for, so we refuse
301 - // instead of falling back to the current user (which allowed login CSRF
302 - // and let a forged state overwrite any user's Google tokens).
303 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended
304 - $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : '';
305 - $state_data = '' !== $state ? $this->consume_oauth_state( $state ) : false;
306 -
307 - if ( ! $state_data ) {
308 - wp_safe_redirect( $this->get_redirect_url_for_admin() );
309 - exit;
241 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
242 + if ( isset( $_GET['state'] ) ) {
243 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
244 + $decoded_state = base64_decode( sanitize_text_field( wp_unslash( $_GET['state'] ) ) );
245 + $state_data = json_decode( $decoded_state, true );
246 + $team_member_id = isset( $state_data['team_member_id'] ) ? $state_data['team_member_id'] : null;
310 247 }
311 248
312 - $team_member_id = absint( $state_data['team_member_id'] );
313 - $initiator = absint( $state_data['initiator'] );
249 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended
250 + $code = sanitize_text_field( wp_unslash( $_GET['code'] ) );
314 251
315 - // The browser finishing the OAuth round-trip must belong to the same
316 - // logged-in user who started it, and that user must still be allowed to
317 - // manage the target team member's connection.
318 - if ( get_current_user_id() !== $initiator
319 - || ( $initiator !== $team_member_id && ! current_user_can( 'edit_user', $team_member_id ) ) ) {
320 - wp_safe_redirect( $this->get_redirect_url_for_admin() );
321 - exit;
252 + if ( ! isset( $team_member_id ) ) {
253 + $team_member_id = get_current_user_id();
322 254 }
323 255
324 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended
325 - $code = sanitize_text_field( wp_unslash( $_GET['code'] ) );
326 -
327 256 try {
328 257 $tokens = $this->exchange_code_for_tokens( $code );
329 258
330 259 if ( ! $tokens || isset( $tokens['error'] ) ) {
@@ -337,19 +266,19 @@
337 266 }
338 267
339 268 update_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, $tokens );
340 269
341 - if ( current_user_can( 'manage_options' ) && $team_member_id === get_current_user_id() ) {
270 + if ( current_user_can( 'manage_options' ) ) {
342 271 booktics_update_option( 'google_calendar_connected', 1 );
343 272 }
344 273
345 - $redirect_url = ( $team_member_id !== get_current_user_id() ) ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
274 + $redirect_url = isset( $team_member_id ) && (int)$team_member_id != get_current_user_id() ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
346 275
347 276 wp_safe_redirect( $redirect_url );
348 277 exit;
349 278
350 279 } catch ( Exception $e ) {
351 - $redirect_url = ( $team_member_id !== get_current_user_id() ) ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
280 + $redirect_url = isset( $team_member_id ) ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
352 281
353 282 wp_safe_redirect( $redirect_url );
354 283 exit;
355 284 }
@@ -519,11 +448,9 @@
519 448 // Update OAuth config
520 449 $this->oauth_config['client_id'] = $google_client_id;
521 450 $this->oauth_config['client_secret'] = $google_client_secret;
522 451
523 - // Bind the flow to this admin with a random, single-use state.
524 - $state = $this->issue_oauth_state( get_current_user_id() );
525 - $auth_url = $this->create_auth_url( $state );
452 + $auth_url = $this->create_auth_url();
526 453
527 454 return $this->response(
528 455 array( 'auth_url' => $auth_url ),
529 456 __( 'Google authentication URL generated successfully.', 'booktics' )
@@ -537,17 +464,15 @@
537 464 * @return WP_Error|WP_HTTP_Response
538 465 */
539 466 public function get_auth_url( $request ) {
540 467 try {
541 - // permission_check() has already confirmed the caller may manage
542 - // this team member; bind the flow to them with a random, single-use
543 - // state rather than an encoded, forgeable user id.
544 - $team_member_id = absint( $request->get_param( 'team_member_id' ) );
545 - if ( ! $team_member_id ) {
546 - $team_member_id = get_current_user_id();
547 - }
468 + // Get any parameters you want to pass through
469 + $state_params = array(
470 + 'team_member_id' => absint( $request->get_param( 'team_member_id' ) ),
471 + );
548 472
549 - $state = $this->issue_oauth_state( $team_member_id );
473 + // Convert to JSON and base64 encode to make it URL-safe
474 + $state = base64_encode( wp_json_encode( $state_params ) );
550 475
551 476 $auth_url = $this->create_auth_url( $state );
552 477
553 478 if ( empty( $auth_url ) ) {