前回の記事、electronにてMicrosoft Azure ADのAccess Tokenの取得まで行いました。しかし、前回の項目では「安全なトークンの保存」であったり、「Refresh Tokenを使ってAccess Tokenを更新」といった項目が実装されていません。

今回の項目では、これらの項目を改造項目として加えて、認証周りを完成させてみたいと思います。Access Tokenが取得できていれば、あとはGraph APIだけでなく、Azureの各種サービスのAPIの実行も可能になります。

今回追加するモジュール

Windowsに於けるelectron向けにKeytarのインストールは非常に手間が掛かります。一度環境を構築してしまえば問題ないのですが、electronやkeytarのバージョン全てが利用できるわけではない点に注意。

社内プロキシーを超える

社内向けのアプリを作る上での実は一番の障害は、「プロキシーサーバ」だと思います。npmにしかり、electron、expressそれぞれにプロキシーを超える設定をする必要がある場合があります(特に大企業の場合、普通にプロキシーサーバ経由になってる場合が多く、これを超える設定を行わないと、外部と通信が出来ない)

electron側の設定

こちらは簡単です。プロキシーサーバもしくはプロキシーのpacファイルの指定を1行追加すれば、外部との通信が可能になります。これを入れておかないと、外部のCSSなどもロードされないので、注意。

図:プロキシー経由させずにDNSエラーが出る

パッケージを追加

今回、expressをプロキシーの背後で動かす為に、httpx-proxy-agent-configというモジュールが必要でした。公式サイトなどでのコマンドラインで打つとインストールが出来ません。以下のコマンドでインストールが可能です。

関連モジュールもインストールされて、これでexpressからプロキシー経由で外に出る事が可能です。また、requestモジュール等にも別途Proxy設定せずとも有効になるので、非常に便利なモジュールです。

また、npmで追加できない場合、githubからファイルを拾ってきて手動で導入も可能です。一応記しておきます。

  1. Githubのページにアクセスして、右上にあるClone or DownloadでZIPでダウンロード
  2. ファイルを解凍する
  3. 解凍したフォルダの名前はhttpx-proxy-agent-configにリネーム
  4. 中に入るとpackageというフォルダがあるので、さらに入る
  5. この中にあるファイル類をhttpx-proxy-agent-configフォルダ直下に全部移動させる
  6. httpx-proxy-agent-configフォルダをプロジェクトのnode_modulesフォルダへ移動する
  7. ターミナルから入って、プロジェクトの中のnode_modulesへ移動。httpx-proxy-agent-configのフォルダに入る
  8. npm installを実行
  9. プロジェクト直下のpackage.jsondependenciesに以下の行を追加する

express側の設定

expressのapp.js側に追加するコードは以下の通り。

たったこれだけです。http_proxyとhttps_proxyは環境によっては同じURLで問題ないかと。ただし、blacklistについてコメントアウトしてありますが、blacklistに入れたものはこのモジュールがブロックするので、注意してください。今回はelectronがlocalhost:3000にアクセスする必要があるので、localhostなどもblacklistには入れません。

以降のexpressの外部への通信はすべてプロキシー経由になります。

Keytarでパスワードの安全保管

Access TokenやRefresh TokenはAPIを叩く場合に渡す非常に重要なコードです。これをプログラムと同じディレクトリのファイルに保存したり、誰でもわかるような場所に素のまま格納するのは、危険です。そこで今回JSONに書き出したファイルを暗号化、その際のパスワードをOSの資格情報マネージャに格納し、保存・取り出しが出来るのが、keytarです(元々electron用ではなく、Node.js用)。

keytarのインストール

Windowsに於いて、keytarはnpmで簡単にインストールできません。リビルドする為にelectron-rebuildが必要です。また、インストール時にnode-gypによるネイティブモジュール問題の解決や、python2.7系でなければ駄目など複雑です。この辺りの環境構築については、こちらのエントリーでまとめています。electronでnode.jsのモジュールでリビルドが必要なケースは必要な環境なので、構築しておきましょう。

ここでは、環境が整っていることを前提に、インストールをする手順だけを記述します。(Windowsでは、electronはv3.0.0, keytarは4.2.1を指定しています)。

  1. コマンドプロンプト(ターミナル)を起動する
  2. npm install keytar@4.2.1keytarをインストール
  3. node-gypによるネイティブモジュールコンパイルが始まる
  4. node-modulesフォルダ内にあるkeytarフォルダに移動する
  5. コマンドプロンプトでnode-gyp configureを実行する
  6. プロジェクトフォルダ直下に戻る
  7. electron-rebuild -w keytarにてkeytarをリビルドする
  8. Rebuild Complateが出れば完了。これで、keytarが使えるようになる
  • electronのバージョン上げたり、Node.jsのバージョンを上げたりした場合にはリビルドが必要になりますので、ご注意ください(また、バージョン上げるとリビルドが失敗する可能性もあります)。
  • ※keytarを使ったアプリをmacOSとWindowsの両方でリリースする場合は、それぞれに環境を作ってビルドするほうが良いと思います。
  • ※64bit Windows上でrebuildした場合、64bit Windows上でしか動作しませんので注意。また、ia32でelectron-packagerでパッケージを作ろうとした場合にも同様のエラーが出ます。

図:Windowsの資格情報マネージャに登録できた様子

JSONファイルの書き出しと暗号化

前回までのコードですと、Access Tokenを直接textに書き出すという暴挙をしていました。Access TokenやRefresh Tokenがそのままなので、これを秘匿させたい。しかし、膨大な量のデータを資格情報マネージャに格納は出来ないので、JSONファイルを生成して於いて、まるごと暗号化(そのパスワードは設定でkeytarを用いて資格情報マネージャに入れておく)という仕組みを考えてみました(よって設定用の画面が必要になります)。

暗号化パスワード設定用画面

設定用の画面として、setting.html、そこで使うjsやcss、images用のフォルダをプロジェクトフォルダ直下に作ります。また、他でも利用する機会があるので、jQueryをnpm i jqueryでインストールしておきましょう。

レンダラプロセス側コード(setting.html)

  • setting.cssは主に画面のデザイン関係のこちらのサイトのCSSを利用しています。
メインプロセス側コード(index.js)

メインプロセス側には、以下の項目を追加しています。

  1. setwindowというBrowserWindowの制御をするコード
  2. IPC通信による資格情報マネージャの読み書きを制御するコード
  3. タスクトレイアイコン関係のコード

以下のコードは追加した部分だけです。

  • タスクトレイにアイコンを表示し、右クリックメニューで設定を出せるようにしています。
  • setting.htmlを開いた時に、指定のkeyのパスワードを取得して返すようにしています。ない場合には空のまま
  • パスワード保存時には、keytarによって資格情報マネージャに格納します。
  • 取得時、格納時共にIDが本来必要ですが、今回の場合IDは不要なので「temp」としています。
  • keyであるservicenameはなるべく被らないものを利用しましょう。

図:このパスワードでaccess tokenを暗号化します

JSONファイル生成と暗号を施す

Node.jsにて暗号化・復号化にCryptoモジュールを利用します(標準装備なので別途インストールは不要)。以前別のエントリーでも復号化だけは実際に作っています。今回は資格情報マネージャに格納されているパスワードをkeyに利用して、AES192bitで生成したJSONデータを暗号化してみたいと思います。

また、生成時に復号化してTokenが切れているかチェックしやすいように、取得時の日付時間および期限の日付時間もJSONに含めて置こうと思います。本来はハッシュ化したワードで暗号・復号化をすべき所ですが、今回は単純にAESで暗号・復号化します。

app.jsのコード

  • 暗号化の為のencryptAes関数を用意。取得したTokenデータ(JSON形式)を暗号化して、user.jsonというファイルで保存します。
  • アクセストークンの期限は1時間後なので、そのデータ(aclimit)を作ってuserdataに含めておく(リフレッシュ時に使用するため)
  • リフレッシュトークンの期限は90日後なので、そのデータ(rtlimit)を作ってuserdataに含めておく(再度認証が必要かどうか確認時に使用するため)
  • 今回復号化のルーチンは、index.js側に用意してるので、こちらには記載していません。
  • 暗号化は、資格情報マネージャに登録してあるパスワードを使って暗号化しています(そのためにkeytarを利用)。
  • 暗号化する場合には、encryptAes関数には、JSON.stringifyでJSON文字列に変換してから渡す必要があります。

図:暗号化されたTokenデータファイル

JSONファイルの復号化とAccess Tokenの取得

Azureの各種APIを叩く為には、Access Tokenが必要です。しかし、前項でTokenデータは暗号化してありますので、利用時には復号化してあげなければTokenデータを取り出せません。

decryptAes関数にてuser.jsonのデータを復号化してコンソールに表示するというものを作ってみました。実運用時はapp.js側に書くことになるかと思います。

app.js側のコード

  • index.js側にも、cryptoモジュールを読み込ませておきます。
  • 復号化の為に、decryptAes関数を用意。user.jsonを暗号化されたまま、まずは取り込みます。
  • 復号化したデータをそのまま返しても、返された側で取り出せないので、Stringで文字列に変換します。
  • 復号化は、資格情報マネージャに登録してあるパスワードを使って復号化しています(そのためにkeytarを利用)。
  • decryptAes関数をcallbackにしておかないと、呼び出し元で呼び出されても返り値を受け取れません。
  • 呼び出し元では以下のようなコードで、呼び出してcallbackで受け取ってコンソール表示しています。

図:無事に復号化して、Access Tokenだけ取り出せた

トークンリフレッシュ

取得したAccess Tokenはおよそ1時間で失効します。其のため、継続的に使うには毎回ログインし直さないといけない。これではあまりにも不便です。そこで用意されているのがRefresh Token。これを使って新しいAccess Tokenを自動的に取得して、継続してアプリを使えるようにする仕組みが、この手のアプリケーションでは必須です。

requestモジュールをインストールする

今回使用するのは、requestというモジュール。passport自体にリフレッシュ機能が備わっていないので、これを使う必要があります。インストール自体はとても簡単。

これだけ。simple-oauth2passport-oauth2-refreshなどのモジュールもあるのですが、どちらも自分の環境では使えなかったので、自力でrequestモジュールで更新するコードを構築しました。

リフレッシュするコード

特定のモジュールがなくとも、requestモジュールで組み立ててあげれば、Refresh Tokenにてトークンの取得が可能です。きちんと、Portal側でスコープや権限を割り当ててあり、テナントIDに間違いがなく、アクセスURLも正しいものであれば、エラーに遭遇せずに済むでしょう。HTML側は今後実際に使うウィンドウになるので、余計なコードを排除しました。

app.js側コード

  • refresh tokenを取得する為のURL(変数ref)に注意してください。
  • サインアウト時にuser.jsonファイルを廃棄するコードを追加してあります。また、現在特定のトークンをrevokeさせるAPIがAzureに存在しないので、このような処置をしています。
  • refreshのリンクをクリックすると、user.jsonを復号化し、リフレッシュトークンの有効期限をチェックした後、renewTokenで新しいAccess Tokenを取得するようにしています。
  • 新しいTokenデータは、encryptAes関数にて再び暗号化されて、user.jsonとして書き出されます。
  • エラー等の通知は、dialogモジュールを利用しています。
  • renewToken関数内で、requestモジュールを使ってPOST通信にて新しいTokenデータを取得しています。
  • AzureのTokenエンドポイントはpayloadをJSON形式で受け付けていないため、access_token=xxxx&refresh_token=xxxxといった形式に変換してから格納しています。
  • 渡すscopeですが、半角スペース区切り。但しこの時半角スペースはURLエンコードしておくことが要求されるので%20で区切ってあります。新規取得時と同じスコープを指定する必要があります。
  • payloadに渡すgrant_typeはrefresh_tokenである必要があります。
  • 無事に認証されて、status200が返ってくれば、新しいトークンが手に入ります。
  • これで、APIを実行する場合、有効期限をチェックし、問題なければそのまま実行、切れていればRefresh Tokenで再取得して実行が可能になります。API叩き放題です。

HTML側コード

  • こちら側は余計なコードを排除し、新たにrefreshを追加しました。

関連リンク

共有してみる: