Google Apps Scriptで複数のOAuth認証を管理する【GAS】
業務でGoogle Apps Scriptを使う際に、各種ウェブサービスの操作を行うにあたってはもはや当たり前になってるOAuth2.0認証をしてリクエストを投げる手法。ライブラリを使って実現しますが、基本的には1つのサービスに対してしか対応していません。複数のウェブサービスを横断してやり取りするには、工夫が必要です。
今回はそれを実現する手法をまとめました。また、複数サービスのユーザ一覧を1枚にまとめる方法についても追記しています。
目次
今回利用するスプレッドシート
- 複数OAuth認証を行う - Google Spreadsheet
これまでも取り上げてきたOAuth2.0認証を行うものなのですが、同時に複数のサービスに対して認証をして、個別にそれらに関する認証情報をキープして、使う際に柔軟に取り出せるようにする必要があります。
事前準備
GAS側の準備
以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。
- スクリプトエディタを開きます。
- サイドバーよりより「ライブラリ」の+ボタンをクリック
- ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
- 今回はバージョンは43を選択してみます。
- 保存ボタンを押して完了
これで、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の内容が異なるので、このような工夫が必要となります。
特定のサービスだけシートから除去する
今回はユーザ一覧を取得して、シートのデータを洗替で書き換えるので、特定のサービス名を持つものを除外するコードを用意してあります。
//特定のサービス名のレコードを一括消去する
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でデータを特定サービスのデータだけ消去したのちに一括で上書きし直す

