← 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 ) ) { |