VBAからBox APIを叩いてみる - IE11廃止対応版
Windows11では既にIE11が使えない為、VBAからのOAuth2.0認証時にIEを立ち上げて認証が出来ない為、こちらのエントリーにあるように「Puppeteerを使って認証するEXE」をNode.jsとPuppeteerを使って作り、クリアさせています。基本的には同じ仕組みなのですが、REST APIを提供するサイトによってちょっとずつ事なるので、困りものですが。
しかし、Windows10でも2022年6月には基本的にIE11が廃止となるため、このままだとBox APIを叩くのに認証が出来ず困ります。という事で、Box API用の認証用EXEを作成し、それをもってしてOAuth2.0を行わせ、Access Tokenを取得するものを作成しました。以下のサイトの改訂版となります。
目次
今回利用するファイル等
新方式が登場しました
IE11の廃止に伴い、SeleniumやNode.jsやらといった手段を使わず、またPuppeteerと同様の手法(CDPを叩く)でVBAとEdge/ChromeのみでOAuth2.0認証する手段が登場しました。スクレイピングも可能になっています。以下のエントリーを参考にしてみてください。この手法は最も制限が無く、もっともすぐれた選択肢になると思います。
事前準備
Box APIを利用するためには、Box Developers Portalより事前準備が必要です。また、企業内で利用する場合には、プロキシーサーバを使っているケースがあるので、そのプロキシーサーバのURLとポート番号がアクセスに必要になります。
プロキシー設定を調べる
企業内で使う場合、ウェブアクセスにプロキシーを使ってる場合には、VBAからアクセスする場合もその設定を利用する必要性があります。プロキシーを経由しなければ外に出ることができないので、プログラムが動作しません。プロキシーの設定はいろいろなパターンがありますが、一般的な設定の調べ方は以下の通り。
サーバーのアドレスとポート番号について、http://を除外して、コロンでポート番号でつなげて利用します。(例:hiroproxy.net:8080)
- コントロールパネルより「インターネットオプション」を開く
- 「接続」タブを開き、「LANの設定」を開く
- この画面でプロキシーサーバの部分にアドレスとポート名が入ってるならばこれを控えておく。
- 場合によっては、詳細設定の中の「HTTP」で指定してるサーバーアドレスとポート番号を控えておく。
- 自動構成スクリプトを使ってる場合、そこに指定されてるアドレスのファイルの中に、様々なプロキシーアドレスが入っていますので、それを一旦ダウンロードして中身をテキストエディタで開いてみる(通常はpacというファイル)
- 5.のケースの場合、pacファイル内はIF文を使ってアクセスするサイト別にプロキシーが設定されてることが多いので、もっとも一般的なサイトアクセスもしくはBoxについてだけ定義している場合には、そのサーバーアドレスとポート番号を控えておく。
図:プロキシー設定がない場合はこの作業は不要です。
Box側の設定を行う
Box側ではClient IDやClient Secret等を作る必要性があります。以下の手順でBox Developers Portalにて作成しましょう。
- Box Developers Portalにログインする
- マイアプリにて「アプリの新規作成」をクリックする
- 次の画面では「カスタムアプリ」をクリックし、次へ進みます。
- 認証方法のページでは、「標準OAuth2.0」をクリックし、次へ進みます。
- アプリの名前は適当に設定し、「アプリの作成」をクリックします。アプリ名は同じものが設定できませんので注意!!
- アプリの表示をクリックして設定データを取得しておきます。
- OAuth2.0資格情報の欄にて、クライアントIDおよびクライアント機密コード(Secret)をコピーして控えて置きます。
- OAuth2.0リダイレクトURIですが、https://localhostでも良いのですが、Internet Explorerを使うので、この設定ですとAccess Tokenが取れない場合があります。(故に今回はこのサイトを指定しました:https://officeforest.org/)
- アプリケーションのスコープでは、許可するアクションにチェックを入れます。
- CORSドメインはウェブなどで利用する場合に許可送信元として入力する場合にそのサイトのドメインを入れておきます。
- 変更を保存するボタンをクリックして完了
図:リダイレクトURLの設定が嵌りどころだったりします。
認証を実行するコード
ここまでの情報でOAuth2.0認証を実行し、Access Token他を取得する準備が整いました。今回はAccessを利用しているので、Access Token、Refresh Token、expireする時間を取得してテーブルに格納します(本来は第三者が容易にこのコードを知られないようにする仕組みを用意しましょう)。IE11で認証するケースとはちょっと異なり、今回は後述のNode.js + Puppeteer + pkgにてWindows用のEXEを作っており、VBAから叩いてAuthcodeを取得する所まではそちらで対応しています。
ソースコード
VBA側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
Option Compare Database 'OAuth認証用 Private Const client_id As String = "ここにクライアントIDを入れる" Private Const client_secret As String = "ここにクライアントシークレットを入れる" Private Const oauthurl As String = "https://account.box.com/api/oauth2/authorize?" Private Const tokenurl As String = "https://api.box.com/oauth2/token" Private Const grant_type As String = "authorization_code" Private Const redirecturl As String = "ここにリダイレクトURLを入れる" 'プロキシURL Const proxyuri As String = "ここにプロキシーのアドレスを入れる" 'Puppeteerで認証コードを実行する Public Function boxAuthorization() 'iniファイルからidとpassを読み込み Dim authcode As String Dim tokenflg As Variant 'WSHの用意 Dim WSH, wExec, sCmd As String, Result As String Set WSH = CreateObject("WScript.Shell") '認証用URLを構築 Dim oauthpage As String Dim param As String 1 'パラメータは&は%26として渡さないと引数が壊れる(スペースは oauthpage = oauthurl & "response_type=code%26client_id=" & client_id & "%26state=authenticated" 'コマンドラインの組み立てと実行 sCmd = CurrentProject.Path & "\index.exe -g " & oauthpage & " -r " & redirecturl Set wExec = WSH.Exec("%ComSpec% /c " & sCmd) 'ステータスを見てループ Do While wExec.status = 0 DoEvents Loop '標準出力内容を取得 authcode = wExec.StdOut.ReadAll 'LFが含まれるかチェック If InStr(authcode, vbLf) > 0 Then '改行コードを除去する authcode = Replace(authcode, vbLf, "") End If 'Access Tokenを取得する tokenflg = getAccessToken(authcode) '終了処理 If tokenflg = True Then 'Access Tokenを取得できた MsgBox "認証が完了しました。" Exit Function Else 'Access Token取得失敗 MsgBox "認証は失敗しましたよ!!残念!" Exit Function End If '終了処理 Set wExec = Nothing Set WSH = Nothing End Function |
- Authenticate Codeを取得するまでのコードです。getAccessToken関数はこれまでと同じで変わりません
- Node.jsで作られてるexeに対して、引数で認証用URLとリダイレクトURLの2つを渡し、相手側のプログラムが完了するまで待機させてあります
- &で引数を区切り渡すと壊れるので、こちらも%26として置き換えて指定(相手側のプログラムでこれは&に変換させています)
- 出力を受け取ったら、今回はDebug.Printで表示していますが、次の「Access Token」を取得する関数へ渡します。
- 嵌りポイントとして、Box APIの場合、code=以下を取得するとauthcodeに「LFの改行コード」が混じってしまい、このまま渡すとAccess Tokenが取得出来ないので、改行コードを除去するコードを加えています。
Node.js側コード
今回は、command-line-argsおよびpuppeteer-coreを利用して、VBAからのコマンドライン引数で認証用URLとリダイレクトURLの2つを取得し、PuppeteerでBox APIの認証⇒Authenticate Codeの取得までを担当させています。それ以降はVBA単体で行えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
//使用するモジュール const puppeteer = require('puppeteer-core'); var fs = require('fs'); const path = require("path"); var shell = require('child_process').exec; var spawnSync = require('child_process').spawnSync; const commandLineArgs = require('command-line-args'); //edge/chromeのパスを取得(ユーザ権限インストール時) const userHome = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"]; var kiteipath = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"; var temppath = path.join(userHome, "AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"); var edgepath = "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"; //chrome場所判定 if (fs.existsSync(kiteipath)) { chromepath = kiteipath } else { if (fs.existsSync(temppath)) { chromepath = temppath; } else { //Chromium Edgeの場合に対応 if(fs.existsSync(edgepath)){ chromepath = edgepath; }else{ //IEを起動してChromeのインストールを促す shellexec('start "" "iexplore" "https://www.google.co.jp/chrome/"') } } } //コマンドライン引数を取得 const optionDefinitions = [ { name: 'geturl', alias: 'g', type: String }, { name: 'getredirect', alias: 'r', type: String } ]; const args = commandLineArgs(optionDefinitions); //puppeteer実行 main(); //puppeteerメイン関数 async function main() { const browser = await puppeteer.launch({ headless: false, executablePath: chromepath, ignoreDefaultArgs: ["--guest",'--disable-extensions','--start-fullscreen','--incognito',], slowMo:100, }); //引数からURLを取得する var tempurl = args.geturl; //redirecturlを取得する var redirect = args.getredirect; //%26を&に変換する var url = tempurl.replace(/%26/g, "&"); //pageを定義 const page = await browser.newPage() const navigationPromise = page.waitForNavigation() //ログインページを開く const response = await page.goto(url) await page.setViewport({ width: 1300, height: 900 }) await navigationPromise //特定要素が出てくるまでウェイト(タイムアウトは90秒を指定) await page.waitForSelector('#login',{timeout:90000}) await navigationPromise //パスワード画面だけはユーザに入力させる var result_input = await waitEvent(page); await page.waitForNavigation('networkidle2') //現在のURLを取得 var allPages = await browser.pages(); var ret = allPages[1].url(); await page.waitForNavigation('networkidle2'); //リダイレクトURLが含まれているかチェックし、含まれるまでループ(二段階認証対応) var rcheck = ret.indexOf(redirect); while(rcheck == -1){ //再度、URLを取得 allPages = await browser.pages(); ret = allPages[1].url(); //リダイレクトURLが含まれているかチェックし、含まれるまでループ rcheck = ret.indexOf(redirect); //rcheckが-1の場合はウェイトを入れる if(rcheck == -1){ await page.waitForNavigation('networkidle2'); } } //code=以降の文字列を取得する var cutman = await ret.substr(ret.indexOf('code=') + 5); //Authenticate Codeを出力する console.log(cutman); //ブラウザを閉じる await browser.close() } //入力イベント待ちをする関数 async function waitEvent(page){ return new Promise(async resolve=>{ //chromeに一時的な関数を作って送り込む(funcmanという名前にしました) //何度も使い回す場合は、event名を重複しないようにする必要があります。 await page.exposeFunction("funcman",()=>{ //result_inputへ値を返す resolve("ログインできた"); }); //承認ボタンにダミーのイベントを追加 await page.evaluate(async () => { var trigger = document.getElementsByClassName("login_submit")[0]; trigger.addEventListener('click', function() { //ダミーのイベント eval('window.funcman();'); }, false); }) }); } |
- 今回はコマンドライン引数は、2つ取得させています。
- 受け取った引数のうち、%26の文字は「&」に変換します。理由はそのままVBA側から&で渡すと引数が壊れる為。
- 上記ボタンのwaitForSelectorの引数timeout:90000を入れてるのは、デフォルト値だと入力などの時間が足らずに、タイムアウトしやすい為(0だとタイムアウト無しになる)
- 承認ボタンが押されるまでウェイトを掛ける為に、waitEventにてダミーの関数を割り当てる挙動を入れています。
- 成功するとリダイレクト先のURLにcode=でAuthenticate Codeが付与されている。このURLは2個目のタブにあるURLなので、allPages[1].url()で取得させています。response.request().redirectChain()などでは取れないので、要注意(responseの中にはそれっぽいURLがいるのだけれど)
- code=以下を取得させていますが、ここにはLFの改行コードが入ってしまってるので、VBA側で除去させています。
- 取り出したら、console.logで出力するとVBA側で拾ってくれる
あとは、VBAの側でこのAuthenticate Codeを引き換えにAccess TokenやRefresh Tokenを取得する仕組みに投げて上げれば良い。
単一実行ファイルを作成する
Node.js 18よりSingle executable applicationsという機能が装備され、標準で単独実行ファイルが作成できるようになりました。結果pkgはプロジェクト終了となっています。よって、以下のエントリーの単一実行ファイルを作成するを参考に、Node18以降はexeファイルを作成することが可能です。
pkgコマンドの注意点
前述のNode.jsとPuppeteer-coreで作ったアプリをEXE化するものとして、pkgを利用しています。類似のものにnexeがあります。最新版pkgでは、Node.js v14に対応しているものの、別の環境でビルドをしようとしたらGitやらpatch.exeやらNASMやらが入っていないとエラー?になった為、ちょっと癖があるプログラムです。
よって、今回はpkg@5.3.3およびNode.js 14.17.5でビルドしています。ビルドする時は以下のコマンドでビルドしないと、evaluate関数あたりでエラーが出てしまうので、注意。
1 |
pkg index.js -t win --public |
作成してできたindex.exeをindex-win.exeにリネームして今回のVBAからは呼び出させています。
nexeコマンドの注意点
同じくEXEを作るnexeですが、keytarなどのネイティブモジュールをパックしてくれなくなってしまったのと、インストール自体がかなり面倒な事になっていたので、改めてここにインストール方法とビルドの注意点を。
注意点としてv3.3.3の時点では
- nexeは自身が用意してる版のNode.jsでしか対応していないので、ビルドする場合にはNode.jsのバージョンを合わせる必要があります(現時点では、v14.15.3まで対応)
- コマンドラインは、nexe index.jsでビルドが出来ます。
- プロキシがある場合には、実行する為にnexeがプロキシを透過できるようにする必要があります以下のコマンドを入れて、npmにプロキシ通過の設定をしておくこと
12npm config set https-proxy https://hogehoge.com:8080npm config set proxy https://hogehoge.com:8080 - keytarなどのネイティブモジュールはEXEにパックされないので、node_modulesのフォルダ毎作成したexeの配布が必要。但し、node_modulesの中はkeytarのフォルダのみあればOK
また、command-line-argsなどにコマンドラインパラメータを渡すようなケースでは、VBAで使う場合はメアド、URLなどの文字列は必ず、Chr(34)などで対象の文字列を括ってからでないと、Account is Requiredというエラーが出て止まります(keytarが原因)。
認証を実行する
ここまでで、OAuth2.0認証のAuthenticate Codeの取得までが装備出来ているので、VBA側で「BoxAuthorization」を実行します。今回のは認証までのコードのみしか入っていませんが、ソレ以降のBox APIを叩くコードやrefresh tokenでAccess Tokenを交換するコードはこれまでと同じです。
実行すると
- 用意しておいた認証用URLとリダイレクトURLをindex.exeに引き渡す
- Puppeteerにてインストール済みのChromeもしくはChromium Edgeが起動し認証ページが開かれる
- ログインをして、承認を実行する
- リダイレクトURL先にcode=付きのURLで飛ばされて、Authenticate Codeを取り出す
という作業が行われます
図:承認画面