get_facebook_app_id(); $app_secret = (string) $secure->get( 'facebook_app_secret', '' ); $return_url = (string) $settings->get_return_url(); if ( '' === $app_id || '' === $app_secret ) { $this->error_redirect( 'Missing app credentials', 'missing_app_credentials'); } // Build encrypted state payload. $state_payload = array( 'v' => 1, 'ts' => time(), 'merchant_site' => home_url(), 'merchant_user_id' => (string) get_current_user_id(), 'wp_nonce' => wp_create_nonce( 'wacc_meta_start' ), 'return_url' => $return_url, 'app_id' => $app_id ); $state_payload['sig'] = $this->sign_state_payload( $state_payload ); $state = ( new Encryptor() )->encrypt( wp_json_encode( $state_payload ) ); $app_id = (string) $state_payload['app_id']; $redirect_uri = (string) $state_payload['return_url']; $scopes = implode( ',', $this->requiredScopes() ); $dialog_url = $this->facebook_url( '/dialog/oauth', array( 'client_id' => $app_id, 'redirect_uri' => $redirect_uri, 'state' => $state, 'response_type' => 'code', 'scope' => $scopes, ) ); wp_safe_redirect( $dialog_url ); exit; } /** * GET handler: Meta OAuth callback. * * Route: admin-post.php?action=wacc_meta_callback&code=...&state=... * * @return void */ public function callback(): void { $code = Request::get( 'code', '' ); $state = Request::get_raw( 'state', '' ); $error_code = Request::get( 'error_code', '' ); $error_message = Request::get( 'error_message', '' ); if ( '' !== $error_code || '' !== $error_message ) { $this->error_redirect( 'Facebook authentication failed', 'authentication_failed', ['error_code' => $error_code, 'error_message' => $error_message]); } if ( '' === $code || '' === $state ) { $this->error_redirect( 'Missing code or state', 'authentication_failed', ['code' => $code, 'state' => $state]); } $state_payload = $this->decrypt_and_validate_state( $state ); $app_secret = (string) (SecureOptions::get_instance())->get( 'facebook_app_secret', '' ); if ( '' === $app_secret ) { $this->error_redirect( 'Missing app secret', 'missing_app_credentials' ); } // Exchange code for a short-lived access token (we store encrypted). $token_response = $this->exchange_code( $code, (string) $state_payload['app_id'], $app_secret, (string) $state_payload['return_url'] ); if ( empty( $token_response['access_token'] ) ) { $this->error_redirect( 'Token exchange failed', 'token_exchange_failed'); } // Store token encrypted on WP side. $secure_options = SecureOptions::get_instance(); $secure_options->set( 'user_access_token', (string) $token_response['access_token'] ); $secure_options->set( 'user_access_token_expires_at', time() + (int) $token_response['expires_in'] ); $return_url = HubPage::url( array( 'tab' => 'settings', 'section' => 'facebook_app', 'connected' => '1' ) ); wp_safe_redirect( $return_url ); exit; } /** * Decrypt + validate state payload (timestamp + integrity signature). * * @param string $state Encrypted state string. * @return array */ protected function decrypt_and_validate_state( string $state ): array { $json = ( new Encryptor() )->decrypt( $state ); $pl = json_decode( (string) $json, true ); if ( ! is_array( $pl ) ) { $this->error_redirect( 'Invalid state payload', 'authentication_failed', ['json' => $json]); } $ts = isset( $pl['ts'] ) ? (int) $pl['ts'] : 0; if ( $ts <= 0 ) { $this->error_redirect( 'Invalid state timestamp', 'authentication_failed', ['json' => $json]); } $now = time(); if ( $ts < ( $now - self::STATE_TTL_SECONDS ) ) { $this->error_redirect( 'State expired', 'authentication_failed', ['json' => $json]); } if ( $ts > ( $now + self::STATE_MAX_FUTURE_SKEW_SECONDS ) ) { $this->error_redirect( 'State timestamp invalid (clock skew)', 'authentication_failed', ['json' => $json]); } $given = isset( $pl['sig'] ) ? (string) $pl['sig'] : ''; $calc = $this->sign_state_payload( $pl ); if ( '' === $given || ! hash_equals( $calc, $given ) ) { $this->error_redirect( 'State signature mismatch', 'authentication_failed', ['json' => $json]); } // Bind state to the initiating user and nonce to mitigate confused-deputy / replay. $user_id = (string) get_current_user_id(); if ( ! isset( $pl['merchant_user_id'] ) || (string) $pl['merchant_user_id'] !== $user_id ) { $this->error_redirect( 'State user mismatch', 'authentication_failed' ); } if ( ! isset( $pl['wp_nonce'] ) || ! wp_verify_nonce( (string) $pl['wp_nonce'], 'wacc_meta_start' ) ) { $this->error_redirect( 'State nonce invalid', 'authentication_failed' ); } return $pl; } /** * Sign state payload for integrity. * * @param array $payload Payload. * @return string */ protected function sign_state_payload( array $payload ): string { $tmp = $payload; unset( $tmp['sig'] ); return hash_hmac( 'sha256', (string) wp_json_encode( $tmp ), $this->hmac_key() ); } /** * Exchange OAuth code for access token. * * @param string $code Code. * @param string $app_id App ID. * @param string $app_secret App secret. * @param string $redirect_uri Redirect URI. * @return array */ protected function exchange_code( string $code, string $app_id, string $app_secret, string $redirect_uri ): array { $url = $this->graph_url( '/oauth/access_token', array( 'client_id' => $app_id, 'client_secret' => $app_secret, 'redirect_uri' => $redirect_uri, 'code' => $code, ) ); $res = wp_remote_get( $url, array( 'timeout' => 15 ) ); $response = new Response($res); if (!$response->isSuccessful()) { return array('error' => $response->getError()); } return $response->getBody(); } /** * Build a facebook.com URL for dialog/oauth. * * @param string $path Path. * @param array $params Params. * @return string */ protected function facebook_url( string $path, array $params ): string { $url = 'https://www.facebook.com/' . $this->graphVersion() . '/' . ltrim( $path, '/' ); return (string) add_query_arg( $params, $url ); } protected function graph_url( string $path, array $params ): string { $url = 'https://graph.facebook.com/' . $this->graphVersion() . '/' . ltrim( $path, '/' ); return (string) add_query_arg( $params, $url ); } /** * HMAC key for signatures. * * @return string */ protected function hmac_key(): string { return (string) wp_salt( 'auth' ); } /** * @param string $signature Signature. * @return string */ protected function auth_cache_key( string $signature ): string { return 'wacc_meta_auth_' . $signature; } protected function error_redirect( string $message, $notice = 'authentication_failed', array $context = [] ): void { Log::error( 'Facebook authentication failed', array_merge( ['message' => $message], $context )); $return_url = HubPage::url( array( 'tab' => 'settings', 'section' => 'facebook_app', 'afromewo_notice' => (string) $notice ) ); wp_safe_redirect( $return_url ); exit; } }