electronでAzure AD認証を行い、Graph APIを叩く – 実装編
前回の記事、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などもロードされないので、注意。
1 2 3 4 5 |
//プロキシーサーバ直の場合 app.commandLine.appendSwitch('proxy-server', 'http://192.168.1.2:8080'); //プロキシーのPACファイルの場合 app.commandLine.appendSwitch('proxy-pac-url', 'http://192.168.1.2:8080/proxy.pac'); |
図:プロキシー経由させずにDNSエラーが出る
パッケージを追加
今回、expressをプロキシーの背後で動かす為に、httpx-proxy-agent-configというモジュールが必要でした。公式サイトなどでのコマンドラインで打つとインストールが出来ません。以下のコマンドでインストールが可能です。
1 |
npm i httpx-proxy-agent-config |
関連モジュールもインストールされて、これでexpressからプロキシー経由で外に出る事が可能です。また、requestモジュール等にも別途Proxy設定せずとも有効になるので、非常に便利なモジュールです。
また、npmで追加できない場合、githubからファイルを拾ってきて手動で導入も可能です。一応記しておきます。
- Githubのページにアクセスして、右上にあるClone or DownloadでZIPでダウンロード
- ファイルを解凍する
- 解凍したフォルダの名前はhttpx-proxy-agent-configにリネーム
- 中に入るとpackageというフォルダがあるので、さらに入る
- この中にあるファイル類をhttpx-proxy-agent-configフォルダ直下に全部移動させる
- httpx-proxy-agent-configフォルダをプロジェクトのnode_modulesフォルダへ移動する
- ターミナルから入って、プロジェクトの中のnode_modulesへ移動。httpx-proxy-agent-configのフォルダに入る
- npm installを実行
- プロジェクト直下のpackage.jsonのdependenciesに以下の行を追加する
1 |
"httpx-proxy-agent-config": "0.0.4", |
express側の設定
expressのapp.js側に追加するコードは以下の通り。
1 2 3 4 5 6 7 8 |
var proxy = require('httpx-proxy-agent-config'); //プロキシーの設定 proxy.install({ http_proxy: 'http://proxyのURL:8080', https_proxy: 'http://proxyのURL:8080', //blacklist: ['localhost'] }); |
たったこれだけです。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を指定しています)。
- コマンドプロンプト(ターミナル)を起動する
- npm install keytar@4.2.1でkeytarをインストール
- node-gypによるネイティブモジュールコンパイルが始まる
- node-modulesフォルダ内にあるkeytarフォルダに移動する
- コマンドプロンプトでnode-gyp configureを実行する
- プロジェクトフォルダ直下に戻る
- electron-rebuild -w keytarにてkeytarをリビルドする
- 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)
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 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script> var $ = jQuery = require("jquery") </script> <script type="text/javascript" src="js/jquery-ui.min.js"></script> <link rel="stylesheet" href="css/jquery-ui.css"> <link rel="stylesheet" href="css/setting.css"> <script src="index.js"></script> <script> $(function() { $( "input[type=submit], a, button" ) .button() .click(function() { }); }); // IPC通信を行う var ipcRenderer = require( 'electron' ).ipcRenderer; window.onload = function () { //受信レンダラーの準備 testAsync(); }; //メインプロセス側からの非同期に通信を受信待機する(1回だけ実行) function testAsync() { //各種雑多なメッセージを受け取る ipcRenderer.on('msg', function(event,arg) { alert(arg); }); //保存済みデータを受け取る ipcRenderer.on('init',function(event,arg){ //受け取ったデータをHTMLのボックスに反映する var array = arg; //パスワードをテキストボックスに格納 document.getElementById("pass").value = array[0]; }) //エラーメッセージの表示 ipcRenderer.on('error',function(event,arg){ //エラーを受け取ったら何もしない }) //現在保存されてるパスワードを取得 ipcRenderer.send('async', "keytar"); } //キャンセル時に設定ウィンドウを閉じる function notsetting(){ ipcRenderer.send('closeset', "setting"); } //セッティング項目を保存する function savesetting(){ //入力値のvalidation var validata = ""; var array = []; //パスワード validata = document.getElementById("pass").value; if(validata == ""){ alert("パスワードが入っていませんよ"); document.getElementById("pass").focus(); return; }else{ array.push(validata); } //メインプロセスに処理を送る ipcRenderer.send('keytar', array); } </script> <title>Azure ADセッティング</title> </head> <body> <!-- セッティング項目を表示 --> <form class="contact_form" action="#" method="post" name="contact_form"> <ul> <li> <h2>プログラムの設定</h2> <span class="required_notification">*印は、必須入力項目です</span> </li> <li> <label for="name">暗号化用パスワード:</label> <input type="password" name="password" placeholder="********" style="width:100px" id="pass" required /> <span class="form_hint">Token暗号化用のパスワードを設定"</span> </li> </ul> </form> <p> <center> <button onClick='savesetting()' id="saveman" class="action" title='設定を保存する'>設定保存</button> <button onClick='notsetting()' id="cancelman" class="action" title='キャンセル'>キャンセル</button> </center> </p> </body> </html> |
- setting.cssは主に画面のデザイン関係のこちらのサイトのCSSを利用しています。
メインプロセス側コード(index.js)
メインプロセス側には、以下の項目を追加しています。
- setwindowというBrowserWindowの制御をするコード
- IPC通信による資格情報マネージャの読み書きを制御するコード
- タスクトレイアイコン関係のコード
以下のコードは追加した部分だけです。
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 138 139 140 141 142 143 144 145 146 |
//追加モジュールの宣言 const keytar = require('keytar'); //タスクトレイ用 let tray = null; // Electronの初期化完了後に実行 app.on('ready', () => { ・・・中略・・・ //トレイのコンテキストメニューを設定 const contextMenu = Menu.buildFromTemplate([ {label:'設定', click(menuItem){ setwindowopen(); //setting.htmlを開く }}, {type:'separator'}, {label:'閉じる',click(menuItem){ //キャッシュを捨てる electron.session.defaultSession.clearCache(() => {}) //設定ウィンドウが開いていたら閉じる if (setWindow && !setWindow.isDestroyed()) { setWindow.close(); setWindow = null; } //閉じる process.exit(0); app.quit(); }} ]); //トレイアイコンを設定 tray = new Tray(__dirname + '/images/azure.ico') //ツールチップの設定 tray.setToolTip("Azure認証をするためだけのアプリ"); //右クリック時にコンテキストメニュー表示をセットする tray.on('right-click',() =>{ //メニューを表示 tray.popUpContextMenu(contextMenu); }); }) //setWindowを再度開くコマンド function setwindowopen(){ //ウィンドウの多重起動防止 if (setWindow && !setWindow.isDestroyed()) { setWindow.show(); setWindow.focus(); return; } // メイン画面の表示。ウィンドウの幅、高さを指定できる setWindow = new BrowserWindow({ 'width': 520, 'height': 250, 'autoHideMenuBar':true, //nodeIntegrationを有効にしないとrenderProcessでrequireを使えない。v5.0.0ではデフォルトで廃止 webPreferences: { nodeIntegration: true }, 'resizable':false, 'fullscreenable':false, 'fullscreen':false, 'alwaysOnTop':true, 'modal':true, //'closable':false }); //初期ページの表示 setWindow.loadURL('file://' + __dirname + '/setting.html'); setWindow.on('closed', function() { electron.session.defaultSession.clearCache(() => {}) setWindow = null; }); // 全てのウィンドウが閉じたときの処理 app.on('window-all-closed', () => { //なにもしない //これをいれておかないと、全部のウィンドウが閉じられるとアプリまで閉じてしまう、。 }); } //設定関係担当 ipcMain.on('async', function( event, args){ //コマンド名によって処理を開始 switch(args){ //資格情報マネージャのパスワードを取得 case "keytar": var array = []; //key名を設定する var servicename = "azuread_auth"; var tempid = "temp"; //key名で探索して返す if(keytar.findPassword(servicename)){ const secret = keytar.getPassword(servicename,tempid); secret.then((result) => { array.push(result); event.sender.send('init', array); return; }); }else{ array.push(""); event.sender.send('init', array); } break; } }); //ウィンドウをコントロール担当 ipcMain.on('closeset', function( event, args ){ //コマンド名によって処理を開始 switch(args){ case "setting": //セッティングウィンドウを非表示にする setWindow.close(); break; default: break; } }); //keytar関係をコントロールする ipcMain.on('keytar', function( event, args ){ //配列データを受け取る var array = args; //サービス名を構築する var servicename = "azuread_auth"; //キーワードを保存する keytar.setPassword(servicename,"temp",array[0]); //セッティングウィンドウを非表示にする setWindow.hide(); }); |
- タスクトレイにアイコンを表示し、右クリックメニューで設定を出せるようにしています。
- setting.htmlを開いた時に、指定のkeyのパスワードを取得して返すようにしています。ない場合には空のまま
- パスワード保存時には、keytarによって資格情報マネージャに格納します。
- 取得時、格納時共にIDが本来必要ですが、今回の場合IDは不要なので「temp」としています。
- keyであるservicenameはなるべく被らないものを利用しましょう。
図:このパスワードでaccess tokenを暗号化します
JSONファイル生成と暗号を施す
Node.jsにて暗号化・復号化にCryptoモジュールを利用します(標準装備なので別途インストールは不要)。以前別のエントリーでも復号化だけは実際に作っています。今回は資格情報マネージャに格納されているパスワードをkeyに利用して、AES192bitで生成したJSONデータを暗号化してみたいと思います。
また、生成時に復号化してTokenが切れているかチェックしやすいように、取得時の日付時間および期限の日付時間もJSONに含めて置こうと思います。本来はハッシュ化したワードで暗号・復号化をすべき所ですが、今回は単純にAESで暗号・復号化します。
app.jsのコード
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 |
//暗号・複合モジュール var crypto = require("crypto"); //暗号化の為の関数 function encryptAes(json){ //サービス名を構築する var servicename = "azuread_auth"; var tempid = "temp"; //key名で探索して返す if(keytar.findPassword(servicename)){ const secret = keytar.getPassword(servicename,tempid); secret.then((result) => { //暗号化 var cipher = crypto.createCipher("aes192", result); var cryptman = Buffer.concat([cipher.update(json), cipher.final()]); //ファイル書き出し fs.writeFileSync("user.json", cryptman); return true; }); }else{ return false; } } //認証フロー passport.use(new strategy(options, (iss, sub, profile, access_token, refresh_token, done) => { try{ if (profile.oid) { //トークン取得日時 var tokenday = new Date(); //アクセストークンリミットの時間を生成(1時間) var aclimit = tokenday.setHours(tokenday.getHours() + 1); //リフレッシュトークンリミットの日付を生成(90日) var rtlimit = tokenday.setDate(tokenday.getDate() + 90); //データを組み立て var userdata = {}; userdata.access_token = access_token; userdata.refresh_token = refresh_token; userdata.atlimit = aclimit; userdata.rtlimit = rtlimit; //Access Tokenをテキストに書き出す //暗号化 encryptAes(JSON.stringify(userdata)); //データを組み立て(index.html側用) const user = { iss, sub, profile, access_token, refresh_token }; //ユーザデータを返す return done(null, user); } //取得データを返す return done(null, false); }catch(err){ //エラートラップ return done(null, err); } } )); |
- 暗号化の為の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側のコード
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 |
//暗号・複合モジュール var crypto = require("crypto"); //復号化の為の関数 function decryptAes(callback){ //user.jsonファイルを取り込む var json = fs.readFileSync("user.json"); //サービス名を構築する var servicename = "azuread_auth"; var tempid = "temp"; //key名で探索して返す if(keytar.findPassword(servicename)){ const secret = keytar.getPassword(servicename,tempid); secret.then((result) => { //復号化 var decipher = crypto.createDecipher('aes192', result); var cryptman = Buffer.concat([decipher.update(json), decipher.final()]); //復号化したデータを返す(Stringで変換する) callback(String(cryptman)); }); }else{ return false; } } |
- index.js側にも、cryptoモジュールを読み込ませておきます。
- 復号化の為に、decryptAes関数を用意。user.jsonを暗号化されたまま、まずは取り込みます。
- 復号化したデータをそのまま返しても、返された側で取り出せないので、Stringで文字列に変換します。
- 復号化は、資格情報マネージャに登録してあるパスワードを使って復号化しています(そのためにkeytarを利用)。
- decryptAes関数をcallbackにしておかないと、呼び出し元で呼び出されても返り値を受け取れません。
- 呼び出し元では以下のようなコードで、呼び出してcallbackで受け取ってコンソール表示しています。
1 2 3 4 5 6 7 8 |
//decryptAes関数を呼び出してAccess Tokenを取り出す decryptAes(function(ret){ //JSONをParseする var json = JSON.parse(ret); //access_tokenを取り出す console.log(json.access_token); }) |
図:無事に復号化して、Access Tokenだけ取り出せた
トークンリフレッシュ
取得したAccess Tokenはおよそ1時間で失効します。其のため、継続的に使うには毎回ログインし直さないといけない。これではあまりにも不便です。そこで用意されているのがRefresh Token。これを使って新しいAccess Tokenを自動的に取得して、継続してアプリを使えるようにする仕組みが、この手のアプリケーションでは必須です。
requestモジュールをインストールする
今回使用するのは、requestというモジュール。passport自体にリフレッシュ機能が備わっていないので、これを使う必要があります。インストール自体はとても簡単。
1 |
npm i request |
これだけ。simple-oauth2やpassport-oauth2-refreshなどのモジュールもあるのですが、どちらも自分の環境では使えなかったので、自力でrequestモジュールで更新するコードを構築しました。
リフレッシュするコード
特定のモジュールがなくとも、requestモジュールで組み立ててあげれば、Refresh Tokenにてトークンの取得が可能です。きちんと、Portal側でスコープや権限を割り当ててあり、テナントIDに間違いがなく、アクセスURLも正しいものであれば、エラーに遭遇せずに済むでしょう。HTML側は今後実際に使うウィンドウになるので、余計なコードを排除しました。
app.js側コード
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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
const { dialog } = require('electron'); //HTTPリクエスト用モジュールの読み込み var request = require('request'); //認証用の設定 var tenant = "ここにテナントのIDを入れる"; var ref = "https://login.microsoftonline.com/"+ tenant +"/oauth2/token" //初期ページ app.get('/', (req, res) => { res.render('index.html'); }); //サインアウトをクリックした時 app.get('/auth/signout', (req, res) => { req.session.destroy((err) => { if (err) { res.send(err) return } //user.jsonファイルを廃棄する try { fs.unlinkSync('user.json'); //ダイアログオプション var options ={ type:'info', title:"サインアウト", button:['OK'], message:'トークンは廃棄されました', detail: "Access Tokenは廃棄されました。" } dialog.showMessageBox(null,options); } catch (error) { //何もしない } //トップページに移動する res.redirect('/') }) } ); //Access Tokenをリフレッシュ app.get('/auth/refresh', (req, res) => { //user.jsonを復号化して各種値を取り出す decryptAes(function(ret){ //JSONデータを受け取る var json = JSON.parse(ret); var rtlimit = json.rtlimit; var access_token = json.access_token; var refresh_token = json.refresh_token; //連想配列を作ってあげる var token ={ rtlimit : rtlimit, access_token : access_token, refresh_token : refresh_token } //tokenリフレッシュの実行 renewToken(json,function(ret){ //更新ステータスを取得する var status = ret[0]; //ステータスによって処理を分岐 if(status == true){ //既存のJSONに値をマージする const user = { "access_token": ret[1].access_token, "refresh_token": ret[1].refresh_token, "atlimit": ret[1].atlimit, "rtlimit": ret[1].rtlimit }; //Access Tokenをテキストに書き出す //暗号化 encryptAes(JSON.stringify(user)); //ダイアログオプション var options ={ type:'info', title:"認証成功", button:['OK'], message:'トークンリフレッシュ成功', detail: "Access Tokenのリフレッシュが成功しました。" } dialog.showMessageBox(null,options); }else{ //ダイアログオプション var options ={ type:'info', title:"エラー", button:['OK'], message:'認証エラーが発生しました', detail:ret[1] } dialog.showMessageBox(null,options); } }); }) }); //暗号化の為の関数 function encryptAes(json){ //サービス名を構築する var servicename = "azuread_auth"; var tempid = "temp"; //key名で探索して返す if(keytar.findPassword(servicename)){ const secret = keytar.getPassword(servicename,tempid); secret.then((result) => { //暗号化 var cipher = crypto.createCipher("aes192", result); var cryptman = Buffer.concat([cipher.update(json), cipher.final()]); fs.writeFileSync("user.json", cryptman); return true; }); }else{ return false; } } //復号化の為の関数 function decryptAes(callback){ var json = fs.readFileSync("user.json"); //サービス名を構築する var servicename = "azuread_auth"; var tempid = "temp"; //key名で探索して返す if(keytar.findPassword(servicename)){ const secret = keytar.getPassword(servicename,tempid); secret.then((result) => { //復号化 var decipher = crypto.createDecipher('aes192', result); var cryptman = Buffer.concat([decipher.update(json), decipher.final()]); //復号化したデータを返す(stringで変換する) callback(String(cryptman)); }); }else{ return false; } } //リフレッシュトークンを使って新しいTokenを取得する function renewToken(token,callback){ //リフレッシュトークンの日付時刻 var dt = new Date(token.rtlimit); //現在の日付時刻 var dt2 = new Date(); //差分を調べる var difftime = dt - dt2; //取得したRefresh Tokenの期限日と現在日付を比較する if(dt <= dt2){ var headers = { "Content-type": "application/x-www-form-urlencoded", } var config = { client_id: clientID, client_secret: clientsecret, scope: "profile%20Files.ReadWrite%20offline_access%20User.Read", redirect_uri: redirecturi, refresh_token: token.refresh_token, grant_type: "refresh_token" } //JSON形式だとNGなので const payload = Object.keys(config).map(key => key + "=" + encodeURIComponent(config[key])).join("&") //POST通信オプションの設定 var options = { url: ref, headers: headers, method: 'POST', body: payload, } //requestにて新しいトークンを取得する request(options, function (error, response, body) { //ステータスコードを取得する var status = response.statusCode; if (!error && status == 200) { //データのパース var json = JSON.parse(body); //トークン取得日時 var tokenday = new Date(); //アクセストークンリミットの時間を生成(1時間) var atlimit = tokenday.setHours(tokenday.getHours() + 1); //リフレッシュトークンリミットの日付を生成(90日) var rtlimit = tokenday.setDate(tokenday.getDate() + 90); //トークンデータを返す var newToken = {}; newToken.atlimit = atlimit; newToken.rtlimit = rtlimit; newToken.access_token = json.access_token; newToken.refresh_token = json.refresh_token; callback([true,newToken]); }else{ //エラー処理 console.log("認証エラー"); callback([false,error]); return; } }); }else{ //リフレッシュトークン切れなので、再認証が必要 callback([false,"Refresh Tokenの期限切れ。再認証が必要です。"]); } } |
- 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側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <script src="app.js"></script> <title>Azure AD認証ダイアログ</title> </head> <body> <div class="container"> <h1>Microsoft Azure 認証</h1> <p> <ul> <li><a href="/auth/signin">signin</a></li> <li><a href="/auth/signout">signout</a></li> <li><a href="/auth/refresh">refresh</a></li> </ul> </div> </body> </html> |
- こちら側は余計なコードを排除し、新たにrefreshを追加しました。
関連リンク
- passport-twitterでのTwitter認証をproxy環境下でも動作させる
- Microsoft Graph (Microsoft365) API のトークンを取得して更新する方法
- maliksahil/expressjs-passport-azure-ad
- How can I get Nodejs twitter-passport behind proxy?
- Cannot connect behind proxy #341
- bloublou2014/httpx-proxy-agent-config
- Passport-azure-ad passport plug-in refresh the token
- Behind proxy! #9
- Outlook のメール、予定表、および連絡先を取得する Node.js アプリの記述
- Microsoft Graph を使った Office 365 Node.js Connect サンプル
- Microsoft Graph を使って Node.js Express アプリを構築する
- ExpressとPassport.jsでOAuth2 (4)有効期限切れのアクセストークンをリフレッシュする
- proxy-agent - npm
- AWS SDK for Node.jsをProxy環境で使う
- Nodejsのtwitter-passportをプロキシの背後で取得するにはどうすればよいですか?
- Node.js 用のプロキシの設定 - AWS SDK for JavaScript
- chimurai/http-proxy-middleware
- Anyway to set proxy setting in passportjs?
- express.jsによるプロキシ
- Proxy with express.js
- Express + Passport と Angular でセッション管理するアプリを作ってみる
- Githubにあげた個人のnpmパッケージをインストールする方法
- OpenID Connect と Azure Active Directory を使用する Web アプリケーションへのアクセスの承認
- passport-azure-ad, validation of tokens
- Azure AD が発行するトークンの有効期間について
- fiznool/passport-oauth2-refresh
- JavaScriptのasync/awaitがPromiseよりもっと良い
- 意外と知られていないJavaScriptのnew Date()の使用方法
- Expose proxy options of request package #16
- Express Js using simple-oauth2 to access Microsoft Graph, possible proxy issue?
- Content-type is not JSON compatible while using Simple OAuth2 with NodeJS
- Refresh token [TypeError] #114
- 5.更新トークンを使用して新しいアクセス トークンを取得する
- Microsoft GraphでOAuthのアクセストークンを更新する仕組みを作る
- TypescriptからMicrosoft Graph API使ってSharePointやOneDrive上のExcelの情報を読み込む
- HTTP POST Body parameters to get OAuth2 token
- AAD: Revoke / Invalidate access tokens #12717