Google Apps Scriptで複数のOAuth認証を管理する【GAS】

業務でGoogle Apps Scriptを使う際に、各種ウェブサービスの操作を行うにあたってはもはや当たり前になってるOAuth2.0認証をしてリクエストを投げる手法。ライブラリを使って実現しますが、基本的には1つのサービスに対してしか対応していません。複数のウェブサービスを横断してやり取りするには、工夫が必要です。

今回はそれを実現する手法をまとめました。また、複数サービスのユーザ一覧を1枚にまとめる方法についても追記しています。

今回利用するスプレッドシート

これまでも取り上げてきたOAuth2.0認証を行うものなのですが、同時に複数のサービスに対して認証をして、個別にそれらに関する認証情報をキープして、使う際に柔軟に取り出せるようにする必要があります。

事前準備

GAS側の準備

以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。

  1. スクリプトエディタを開きます。
  2. サイドバーよりより「ライブラリ」の+ボタンをクリック
  3. ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
  4. 今回はバージョンは43を選択してみます。
  5. 保存ボタンを押して完了

これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。

図:ライブラリを追加した様子

複数サービスの認証情報ファイル

スクリプトプロパティに保存しても良いのですが、今回は複数のウェブサービスのClient IDやSecret、リクエストURLなどを一纏めにしたoauth.jsonというファイルを作成し、ドライブに配置してその情報を取得して使うようにしてあります。

oauth.jsonの仕様は以下のような感じ。利用するウェブサービスによっては項目が増えると思うので、そのへんを上手くまとめておきましょう。

{
	"webex":{
		"clientid":"ここにClient IDをいれる",
		"secret":"ここにシークレットを入れる",
		"authurl":"ここにAuthorization URLを入れる",
		"token":"ここにTokenリクエストURLを入れる",
		"scope":"ここにスコープを入れる",
		"orgId":"ここに組織IDをいれる"
	},
	"microsoft365":{
		"clientid":"ここにClient IDをいれる",
		"secret":"ここにシークレットを入れる",
		"authurl":"ここにAuthorization URLを入れる",
		"token":"ここにTokenリクエストURLを入れる",
		"scope":"ここにスコープをいれる"
	}
}

今回は2つのウェブサービス。殆ど似ているのですが、webexだけorgIdが必要なので追加しています。また、ウェブサービスによっては当然scopeというものがなかったりするので、サービス毎にきちんと値を入れたり外したりが必要です。このファイルを読み込んで利用します。

ソースコード

認証を行うフロー

認証に用いるoauth.jsonファイルのIDを冒頭に記述しておき、それぞれのサービス認証用にstartoauthを呼び出す関数を用意しておく。startoauth関数は引数でサービス名を受け取って処理をするように作っておく。

//clientidのまとめファイル
var oauthjson = "oauth.jsonファイルのIDをここに入力";

//webex認証
function webexauth(){
  //Webexとして認証を開始
  startoauth("webex");
}

//Microsoft365認証
function m365auth(){
  //microsoft365として認証を開始
  startoauth("microsoft365");
}

実際に認証をする側のコードもこれまでのコードよりも工夫が必要です。

//OAuth認証を実行kする
function startoauth(propman){
  //UIを取得する
  var ui = SpreadsheetApp.getUi();

  //tempauthを書き換える
  var prop = PropertiesService.getScriptProperties();
  prop.setProperty("tempauth",propman);
  
  //認証済みかチェックする
  var service = checkOAuth();
  if (!service.hasAccess()) {
    //認証画面を出力
    var output = HtmlService.createHtmlOutputFromFile('template').setHeight(450).setWidth(500).setSandboxMode(HtmlService.SandboxMode.IFRAME);
    
    ui.showModalDialog(output, 'OAuth2.0認証');
  } else {
    //認証済みなので終了する
    ui.alert("すでに認証済みです。");
  }
}

//アクセストークンURLを含んだHTMLを返す関数
function authpage(){
  var service = checkOAuth();
  var authorizationUrl = service.getAuthorizationUrl();

  var html = "<center><b><a href='" + authorizationUrl + "' target='_blank' onclick='closeMe();'>アクセス承認</a></b></center>"
  return html;
}

//認証チェック
function checkOAuth() {
  //プロパティからサービス名を取得する
  var prop = PropertiesService.getScriptProperties();
  var propname = prop.getProperty("tempauth")
  
  //propnameに応じたoauth認証データを取り出す(oauth.jsonのデータを取り出す)
  var file = DriveApp.getFileById(oauthjson);
  var oauthdata = JSON.parse(file.getBlob().getDataAsString("UTF-8"));
 
  //clientid, secret, authurl, tokenurlを取り出す
  let authurl = oauthdata[propname].authurl;
  let tokenurl = oauthdata[propname].token;
  let clientid = oauthdata[propname].clientid;
  let secret = oauthdata[propname].secret;

  //各サービス固有の情報を元に分岐
  let scope = "";
  switch(propname){
    case "webex":
      //スコープを取得
      scope = oauthdata[propname].scope;

      return OAuth2.createService(propname)
      .setAuthorizationBaseUrl(authurl)
      .setTokenUrl(tokenurl)
      .setClientId(clientid)
      .setClientSecret(secret)
      .setCallbackFunction("authCallback") //認証を受けたら受け取る関数を指定する
      .setPropertyStore(PropertiesService.getScriptProperties())  //スクリプトプロパティに保存する
      .setScope(scope)
    case "microsoft365":
      //スコープを取得
      scope = oauthdata[propname].scope;

      return OAuth2.createService(propname)
      .setAuthorizationBaseUrl(authurl)
      .setTokenUrl(tokenurl)
      .setClientId(clientid)
      .setClientSecret(secret)
      .setCallbackFunction("authCallback") //認証を受けたら受け取る関数を指定する
      .setPropertyStore(PropertiesService.getScriptProperties())  //スクリプトプロパティに保存する
      .setScope(scope);
      break;

  }
}

//認証コールバック
function authCallback(request) {
  var service = checkOAuth();
  var isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput("認証に成功しました。ページを閉じてください。");
  } else {
    return HtmlService.createHtmlOutput("認証に失敗しました。");
  }
}

//ログアウト
function reset(propname) {
  //プロパティにサービス名をセットする
  var prop = PropertiesService.getScriptProperties();
  prop.setProperty("tempauth",propname);

  checkOAuth().reset();
  SpreadsheetApp.getUi().alert("ログアウトしました。")
}
  • startoauth、resetではtempauthのプロパティの値をサービス名で書き換えておく。
  • authpage、authCallbackは特に変更なし
  • checkOAuthでは呼び出すサービス名をtempauthからロードしておく
  • checkOAuthではDriveAppでファイルを読み込ませた後に、共通項目のClient ID, Secret, authurl, tokenurlを取得しておく
  • サービス名で分岐させてサービスの数だけOAuth2.createServiceを用意しておく。
  • setPropertyStoreでPropertiesService.getScriptProperties()を指定していますが、 PropertiesService.getUserProperties(); とすればユーザ毎に認証情報が保存され読み出されるマルチユーザ仕様になります。
  • マルチユーザで使う場合は、複数名同時にアクセスしてくる可能性も考えて、tempauthもgetUserProperties管理にすると良いでしょう。また、スプレッドシートへの書き出しは、排他制御を用いるとデータが壊れずに済みます。

こうすることで、1つの処理系で複数のOAuth2.0認証のやり取りが出来るようになります。サービス毎にリクエストする度にcheckOAuthが呼び出されるわけで、リクエスト前にtempauthの値を各呼び出し元で書き換える手間はありますが、キレイに認証データが格納されます。

サービス毎にOAuth2.createServiceの内容が異なるので、このような工夫が必要となります。

排他制御でGoogle Apps Scriptを安全に実行【GAS】

特定のサービスだけシートから除去する

今回はユーザ一覧を取得して、シートのデータを洗替で書き換えるので、特定のサービス名を持つものを除外するコードを用意してあります。

//特定のサービス名のレコードを一括消去する
function clearServiceRec(servicename){
  //シートを取得する
  let prop = PropertiesService.getScriptProperties();
  let ss = SpreadsheetApp.openById("ここにこのファイルのIDをいれる")

  //シートデータを一括で取得する
  var license = ss.getSheetByName("service")
  var clearman = license.getRange("A2:E")
  var sheet = clearman.getValues();

  //servicenameに一致するレコードを配列から除外する
  const new_array = sheet.filter(function(array){
    return array[3] != servicename
  });

  //シートデータをクリアする
  clearman.clearContent();

  //シートデータを貼り付ける
  let lastColumn = new_array[0].length;  //カラムの数を取得する
  let lastRow = new_array.length;      //行の数を取得する
  license.getRange(2,1,lastRow,lastColumn).setValues(new_array); 

  //return
  return 0;
}
  • サービス名を指定してそのサービス名が含まれるデータを除外するコードです
  • 一括でデータをsheetに読み込んでおき、配列からfilterを使って除外する
  • シートのデータをクリアした後に除外した新しい配列で上書きする

サービスを取得してシートに書き出す

以下はMicrosoft365のユーザ一覧を取得して書き出すコードです。

//Microsoftアカウントのユーザリストを取得する
function getM365Users() {
  //uiを取得する
  let ui = SpreadsheetApp.getUi();

  //サービス名を指定
  let servicename = "microsoft365";

  //シートを取得する
  let prop = PropertiesService.getScriptProperties();
  let ss = SpreadsheetApp.openById("ここにこのファイルのIDを入れる")
  let sheet = ss.getSheetByName("service");

  //propnameを書き換える
  prop.setProperty("tempauth",servicename);

  //トークン確認
  var service = checkOAuth();

  if(service.hasAccess()) {
    //エンドポイントを構築
    var endpoint = "https://graph.microsoft.com/beta/users";

    //リクエストヘッダ
    let header = {
      "Authorization": 'Bearer ' + service.getAccessToken(),
      "content-type": "application/json"
    }

    //リクエストオプション
    let options = {
      method: "GET",
      headers: header,
      muteHttpExceptions: true
    }

    //リクエスト実行
    const response = UrlFetchApp.fetch(endpoint, options);

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    //リクエスト結果を配列に格納する
    var array = [];

    //レスポンスデータを取り出す
    const retman = result.value;

    for(var i = 0;i<retman.length;i++){
      //レコードを一個取り出す。
      let rec = retman[i];
      
      //書き込み用一時配列を用意
      let temparr = [
        rec.id,
        rec.displayName,
        rec.mail,
        servicename,
        "",
      ];

      //arrayに追加する
      array.push(temparr)
    }

    //対象のサービスのデータをまず削除する
    let ret = clearServiceRec(servicename);

    //スプレッドシートの最終行を取得する
    let endrow =  Number(sheet.getLastRow()) + 1;

    //スプレッドシートに書き出しする
    let lastColumn = array[0].length;  //カラムの数を取得する
    let lastRow = array.length;      //行の数を取得する
    sheet.getRange(endrow,1,lastRow,lastColumn).setValues(array); 

    //終了
    ui.alert(servicename + "のユーザ一覧を出力しました。")

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}
  • OAuth認証データを呼び出す前に、tempauthに利用するサービス名を書き込んでおく必要があります。
  • checkOAuthでtempauthの値を元に値を取り出してリクエストする
  • clearServiceRecでデータを特定サービスのデータだけ消去したのちに一括で上書きし直す

関連リンク

コメントを残す

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

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