OAuthによる認可が必要な外部サービスとの連携Pieceを作る
OAuthによるユーザーの認可が必要な外部サービスと連携するPieceを作成する方法について説明します。
OAuthの対応状況
RFC6749(The OAuth 2.0 Authorization Framework)で定義されている全4フローのうち、現在、Riiiverでは認可コードフローにのみ対応しています。また、OAuth1.0はサポートしていません。
各種認可フローへの対応状況
フロー名 | 対応 | 備考 |
---|---|---|
認可コード | ○ | |
インプリシット | - | |
リソースオーナー・パスワード・クレデンシャルズ | - | |
クライアント・クレデンシャルズ | - |
利用したい外部サービスの認可サーバーのトークンエンドポイントへリクエストする際のクライアント認証については、client_secret_post
および、client_secret_basic
に対応しています。[参考]
その他、Google APIでは、access_tokenをリクエストする際の方式がRFCに完全準拠ではありませんが、サポートしています。
リクエスト方式 | 対応 | post-typeの指定 | 備考 |
---|---|---|---|
client_secret_post | ○ | form_urlencoded | |
client_secret_basic | ○ | base64 | |
client_secret_jwt | - | ||
private_key_jwt | - | ||
tls_client_auth | - | ||
self_signed_tls_client_auth | - | ||
その他(Google API) | ○ | json |
Googleカレンダーと連携するPieceを作成する場合
OAuthによる認可が必要な外部サービスと連携するPieceを作成するには以下が必要です。ここでは、OAuthによる認可フローが必要な外部サービスとして、Googleカレンダーと連携するPieceを作成する場合を例に、必要となるファイルについて、順に説明していきます。
- OAuthのJSONファイル(oauth.json)
- PieceのJSONファイル(piece.json)
- node_modules ( request, request-promise をインストール)
- PieceCoreのjsファイル(PieceCore.js)
OAuthのJSONファイルについて
まず、oauth.jsonファイルの中身を見てみましょう。
{ "authType": ["oauth2"], "postType": "json", "oauth2": { "authorizedUrl": "https://accounts.google.com/o/oauth2/auth", "tokenUrl": "https://accounts.google.com/o/oauth2/token", "redirectUri": "https://builder.developer.riiiver.com/service_oauth/", "clientId": "〜.apps.googleusercontent.com", "clientSecret": "XXXXXXXXXXXXXXXXXXXXXXX", "scope": "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.readonly" } }
このファイルには、外部サービスの認可サーバーにリクエストをする際に必要となる情報を記述します。
"authType"
はOAuth2.0を利用する場合、"oauth2"
を指定してください。ただし、前述のとおり、現在Riiiverで利用できる"authType"
は"oauth2"
のみです(参照ページ: PieceJSONKeys)。"postType"
で、認可サーバーへのデータ送信の形式を指定します。利用する外部サービスのドキュメントを確認し、こちらの表の中から適切な値を指定してください。今回はGoogle APIを使うので"json"
を指定しています。"oauth2"
の中に、認可フローに必要な情報を記述します。
oauth2の中で指定するパラメータ
keys | description |
---|---|
authorizedUrl | 外部サービスが指定する認可用URL |
tokenUrl | 外部サービスが指定するトークン発行用URL |
redirectUri | 認可サーバーからのレスポンスの受け取り口(変更不要) |
clientId | 外部サービスに発行してもらうID |
clientSecret | 外部サービスに発行してもらうPW |
scope | 権限の付与を許可する範囲 |
state | 連携するサービスによっては必須な場合があります。ドキュメントを確認してください。 |
PieceBuilderにてPieceJSONをアップロードすると、PieceCoreのjsファイル(正確にはzip形式)、icon.pngなどの他に「OAuthファイルの追加」という項目がありますので、そちらからアップロードしてください。Pieceのアップロード方法は「つくったPieceをアップロードする」をご覧ください。
PieceのJSONファイルについて
次に、piece.jsonファイルについて説明します。"ServiceProxy"
に以下のように、"authType"
の項目を追加し、"oauth2"
を指定します。OAuth連携のために必要な記述はこれだけです。前項で説明したOAuthのJSONファイルとの紐付けは、サーバ側で処理していますので、piece.jsonのどこかでoauth.jsonのファイル名を指定する必要はありません。
"serviceProxy": { "service": "googleCalendarWatch", "authType": [ "oauth2" ] }
piece.jsonファイル全体のソースコードは下記のようになります。
{ "title": { "ja": "Google カレンダー", "en": "Google calendar" }, "version": "0.0.1", "sdkVersion": "0.0.1", ・・・中略・・・, "blockType": "service", "executor": "RBCCommonWebServiceExecutor", "serviceProxy": { "service": "googleCalendarWatch", "authType": [ "oauth2" ] }, "output" : { "type" : "object", "properties": { ・・・中略・・・ } } }, "categoryIds": [ "cat_0007" ] }
node_modulesの準備
request-promiseのインストール
request-promiseは、HTTP 通信による情報取得を簡単に行えるパッケージ です。 request
パッケージに依存しているため、request
パッケージも一緒にインストールしておく必要があります。
既に、APIを使ったS Pieceを作ったことのある方は、request
パッケージはインストール済みのはずなので2行目のコマンドのみ実行してください。
npm install request npm install request-promise
request-promiseの使い方
Googleのhtmlを取得する場合は以下のような構成にします。 .then
に成功したときの処理、.catch
に失敗したときの処理を記述します。
var rp = require('request-promise'); rp('http://www.google.com') .then(function (htmlString) { // Process html... }) .catch(function (err) { // Crawling failed... });
request-promiseの基本的な使い方はこちらを参考にしてください。
PieceCore.jsについて
最後に、PieceCore.jsについて説明します。Googleカレンダーからイベントを最大3つまで取得して、開始時間順にソートするサンプルコードを以下にご紹介します。
const requestPromise = require('request-promise'); exports.handler = async (event) => { if (event.call === 'lambda') { console.log('CALLED:LAMBDA'); /* 外部モジュールを使う場合に記入してくだい。 ex : if ( xxxx !== null ){} // xxxx : 生成したインスタンス */ if (requestPromise !== null) { } return; } console.log(`googleCalendarWatch request data: ${ JSON.stringify(event) }`); let date = event.userData.date; let responseData; return requestPromise({ 'method': 'GET', 'uri': 'https://app.riiiver.com/api/proxyBlockId', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': event.userData.credential, }, 'qs': { 'serviceProxy': event.serviceProxy, }, 'resolveWithFullResponse': true, }).then(data => { if (data.statusCode !== 200) { responseData = { status: data.statusCode, body: { message: data.body } }; return responseData; } console.log(`GET proxyBlockId responseData: ${data.body}`); const blockId = JSON.parse(data.body).blockId; return requestPromise({ 'method': 'GET', 'uri': 'https://app.riiiver.com/api/clientAuthInfo', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': event.userData.credential, }, 'qs': { 'blockId': blockId, 'uuid': event.userData.uuid }, 'resolveWithFullResponse': true, }).then(data => { if (data.statusCode !== 200) { console.log(`clientAuthInfo`); responseData = { status: data.statusCode, body: { message: data.body } }; return responseData; } const body = JSON.parse(data.body); const expires_in = body.oauth2.expires_in; const now = Math.floor(new Date().getTime() / 1000); if (expires_in <= (now - (60 * 5))) { return requestPromise({ 'method': 'PUT', 'uri': 'https://app.riiiver.com/api/updateOAuthToken', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': event.userData.credential, 'Content-Type': 'application/json', }, 'qs': { 'blockId': blockId, 'uuid': event.userData.uuid }, 'body': JSON.stringify({ 'refresh_token': body.oauth2.refresh_token }), 'resolveWithFullResponse': true, }).then(data => { if (data.statusCode !== 201) { responseData = { status: data.statusCode, body: { message: data.body } }; return responseData; } const access_token = JSON.parse(data.body).access_token; return getEvents(access_token, event, date).then(response => { responseData = response; return responseData; }); }); } else { const access_token = body.oauth2.access_token; return getEvents(access_token, event, date).then(response => { responseData = response; return responseData; }); } }) .catch(error => { responseData = { status: error.statusCode, body: { message: error.message } }; return responseData; }); }).catch(error => { responseData = { status: error.statusCode, body: { message: error.message } }; return responseData; }).finally(() => { console.log(`getGoogleCalendarEvents response data: ${ JSON.stringify(responseData) }`); }); }; const getEvents = async (access_token, event, today) => { // dateのフォーマット "yyyy-MM-dd'T'HH:mm:ssZZZ" return new Promise(resolve => { return requestPromise({ 'method': 'GET', 'uri': 'https://www.googleapis.com/calendar/v3/calendars/primary/events', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': `Bearer ${access_token}`, }, 'qs': { // 'maxResults': 3, // 最大3件 'timeMax': getDateInfo(today) + "T23:59:59" + getTimeZoneInfo(today), 'timeMin': today }, 'resolveWithFullResponse': true, }).then(data => { console.log(`API response data: ${ data.body }`); const items = JSON.parse(data.body).items; const date = new Date(today).getTime(); let plans = []; let cnt = 0; items.forEach((v) => { if (v.status === 'confirmed') { const startDate = new Date(v.start.dateTime); const startTime = getTimeInfo(v.start.dateTime); let endTime = getTimeInfo(v.end.dateTime); if (cnt < 3 && date < startDate.getTime()) { cnt++; console.log(`Events: ${JSON.stringify(v)}`); //スケジュールの終わりが日をまたぐ場合は24:00とする let startDay = getDayInfo(v.start.dateTime); let endDay = getDayInfo(v.end.dateTime); if(startDay != endDay){ endTime = "24:00"; } plans.push({"startDateTime": startTime, "endDateTime": endTime}); } } }); // スケジュールを開始時間でソートする plans.sort(function(a, b) { // 比較するためDate型を生成する。(時間を比較したいだけなので年月日は適当で良い) let start1 = new Date("2019/01/01 " + a.startDateTime); let start2 = new Date("2019/01/01 " + b.startDateTime); if (start1.getTime() > start2.getTime()) { return 1; } else { return -1; } }); return resolve({ status: 200, body: { "plans": plans } }); }).catch(error => { return resolve({ status: error.statusCode, body: { message: error.message } }); }); }); }; // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getTimeInfo (date) { var index = date.indexOf('T') + 1; // HH:mmを抽出して返却 return date.substring( index, index + 5 ); } // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getDateInfo (date) { var index = date.indexOf('T'); // yyyy-mm-ddを抽出して返却 return date.substring( 0, index ); } // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getDayInfo (date) { var index = date.indexOf('T'); // yyyy-mm-ddを抽出して返却 return date.substring( index -2, index ); } // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getTimeZoneInfo (date) { // ZZZを抽出して返却 return date.substring( date.length - 5, date.length ); }
サンプルコードの説明
このサンプルコードでは、OAuth認可によって取得したaccess_token
を用いて、APIを叩き、そのレスポンスから次のPieceへ送るoutputを生成しています。
以下、サンプルコードの構成を分けて、順番に説明します。
1. ブロックID(Piece ID)を取得します。`https://app.riiiver.com/api/proxyBlockId`
return requestPromise({ 'method': 'GET', 'uri': 'https://app.riiiver.com/api/proxyBlockId', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': event.userData.credential, }, 'qs': { 'serviceProxy': event.serviceProxy, }, 'resolveWithFullResponse': true, }).then(data => { if (data.statusCode !== 200) { responseData = { status: data.statusCode, body: { message: data.body } }; return responseData; } console.log(`GET proxyBlockId responseData: ${data.body}`); const blockId = JSON.parse(data.body).blockId; … /** * 次項以降で説明する処理が続きます */ }).catch(error => { /** * ブロックID(Piece ID)を正常に取得できなかった場合の処理 */ responseData = { status: error.statusCode, body: { message: error.message } }; return responseData; }).finally(() => { console.log(`getGoogleCalendarEvents response data: ${ JSON.stringify(responseData) }`); }); };
2. Riiiverのサーバーに保存されているユーザーのAuth情報を取得します。`https://app.riiiver.com/api/clientAuthInfo`
return requestPromise({ 'method': 'GET', 'uri': 'https://app.riiiver.com/api/clientAuthInfo', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': event.userData.credential, }, 'qs': { 'blockId': blockId, 'uuid': event.userData.uuid }, 'resolveWithFullResponse': true, }).then(data => { if (data.statusCode !== 200) { console.log(`clientAuthInfo`); responseData = { status: data.statusCode, body: { message: data.body } }; return responseData; } … /** * 次項以降で説明する処理が続きます */}).catch(error => { /** * サーバーにAuth情報が保存されていないなど、正常に取得できなかった場合の処理 */ responseData = { status: error.statusCode, body: { message: error.message } }; return responseData; }); …
3. `access_token`を用いて実際にAPIを叩いていきます。`access_token`の有効期限が切れているかどうかを判断し、切れている場合は、`refresh_token`を用いて`access_token`を取得しなおすよう分岐を入れています。`https://app.riiiver.com/api/updateOAuthToken`
{ … const body = JSON.parse(data.body); const expires_in = body.oauth2.expires_in; const now = Math.floor(new Date().getTime() / 1000); /** * access_tokenの有効期限が切れていないか判断する * expires_in(tokenの有効期限)が今と比較して小さい * すなわち、期限が切れているなら、access_tokenを再取得する */if (expires_in <= (now - (60 * 5))) { return requestPromise({ 'method': 'PUT', 'uri': 'https://app.riiiver.com/api/updateOAuthToken', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': event.userData.credential, 'Content-Type': 'application/json', }, 'qs': { 'blockId': blockId, 'uuid': event.userData.uuid }, 'body': JSON.stringify({ 'refresh_token': body.oauth2.refresh_token }), 'resolveWithFullResponse': true, }).then(data => { if (data.statusCode !== 201) { responseData = { status: data.statusCode, body: { message: data.body } }; return responseData; } const access_token = JSON.parse(data.body).access_token; /** * (★1)再取得したaccess_tokenで処理を行う * 実際に自分でPieceを作る際はここと(★2)の処理を変更する */ return getEvents(access_token, event, date).then(response => { responseData = response; return responseData; }); }); } else { const access_token = body.oauth2.access_token; /** * (★2)access_tokenの有効期限が切れていなかった場合の処理 */ return getEvents(access_token, event, date).then(response => { responseData = response; return responseData; }); } …
4. Googleカレンダーからイベントを最大3つまで取得したのち、開始時間順にソートしてoutputを生成します。
/** * (★1, 2)で呼んでいる処理 */const getEvents = async (access_token, event, today) => { // dateのフォーマット "yyyy-MM-dd'T'HH:mm:ssZZZ" return new Promise(resolve => { return requestPromise({ 'method': 'GET', 'uri': 'https://www.googleapis.com/calendar/v3/calendars/primary/events', 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': `Bearer ${access_token}`, }, 'qs': { //'maxResults': 3, // 最大3件 'timeMax': getDateInfo(today) + "T23:59:59" + getTimeZoneInfo(today), 'timeMin': today }, 'resolveWithFullResponse': true, }).then(data => { console.log(`API response data: ${data.body}`); const items = JSON.parse(data.body).items; const date = new Date(today).getTime(); let plans = []; let cnt = 0; items.forEach((v) => { if (v.status === 'confirmed') { const startDate = new Date(v.start.dateTime); const startTime = getTimeInfo(v.start.dateTime); let endTime = getTimeInfo(v.end.dateTime); if (cnt < 3 && date < startDate.getTime()) { cnt++; console.log(`Events: ${JSON.stringify(v)}`); //スケジュールの終わりが日をまたぐ場合は24:00とする let startDay = getDayInfo(v.start.dateTime); let endDay = getDayInfo(v.end.dateTime); if (startDay != endDay) { endTime = "24:00"; } plans.push({ "startDateTime": startTime, "endDateTime": endTime }); } } }); // スケジュールを開始時間でソートする plans.sort(function (a, b) { // 比較するためDate型を生成する。(時間を比較したいだけなので年月日は適当で良い) let start1 = new Date("2019/01/01 " + a.startDateTime); let start2 = new Date("2019/01/01 " + b.startDateTime); if (start1.getTime() > start2.getTime()) { return 1; } else { return -1; } }); return resolve({ status: 200, body: { "plans": plans } }); }).catch(error => { return resolve({ status: error.statusCode, body: { message: error.message } }); }); }); }; // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getTimeInfo(date) { var index = date.indexOf('T') + 1; // HH:mmを抽出して返却 return date.substring(index, index + 5); } // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getDateInfo(date) { var index = date.indexOf('T'); // yyyy-mm-ddを抽出して返却 return date.substring(0, index); } // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getDayInfo(date) { var index = date.indexOf('T'); // yyyy-mm-ddを抽出して返却 return date.substring(index - 2, index); } // date: 日付文字列(yyyy-mm-ddTHH:mm:ssZZZ) function getTimeZoneInfo(date) { // ZZZを抽出して返却 return date.substring(date.length - 5, date.length); }
付録:Googleスプレッドシートへ書き込むPiece
ここまで、Googleカレンダーからデータを取得するPieceを例に説明しました。応用例として、Googleスプレッドシートにデータを書き込むPieceについて見ていきましょう。サンプルコードの説明1〜3で説明した項目はほぼそのまま流用できます。access_tokenを用いて実際にAPIを叩く処理(3の(★1, 2)と4)を書き換えれば作成できます。
今回は、preferenceでスプレッドシートのID(sheetId
)と入力内容(input
)をユーザーに設定してもらうことを想定した場合のコードを下記に示します。
const appendCellsRequest = async (access_token, event, sheetId, input) => { return new Promise( resolve => requestPromise({ 'method': 'POST', 'uri': `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}:batchUpdate`, 'headers': { 'User-Agent': event.userData.userAgent, 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, 'body': { "requests": [{ "appendCells": { "sheetId": 0, "rows":[{ "values":[ {"userEnteredValue": {"numberValue": input}} ] }], "fields": "userEnteredValue" } }] }, 'resolveWithFullResponse': true, 'json': true }).then(response => { console.log(response); return resolve({ status: 200, body: {} }); }).catch(error => { return resolve({ status: error.statusCode, body: { message: error.message } }); }); ); };
また、スプレッドシートへの書き込み権限を許可するため、oauth.json
内のscopeを下記のように変更する必要があります。加えて、事前にSheets APIを有効化しておく必要があります。
"scope": "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/spreadsheets",