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などもロードされないので、注意。

//プロキシーサーバ直の場合
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というモジュールが必要でした。公式サイトなどでのコマンドラインで打つとインストールが出来ません。以下のコマンドでインストールが可能です。

npm i 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に以下の行を追加する
"httpx-proxy-agent-config": "0.0.4",

express側の設定

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

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を指定しています)。

  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)
<!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)

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

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

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

//追加モジュールの宣言
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のコード
//暗号・複合モジュール
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側のコード

//暗号・複合モジュール
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で受け取ってコンソール表示しています。
//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自体にリフレッシュ機能が備わっていないので、これを使う必要があります。インストール自体はとても簡単。

npm i request

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

リフレッシュするコード

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

app.js側コード

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側コード

<!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を追加しました。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)