Creating Pieces that work with external services requiring OAuth authorization
This section describes how to create Pieces that work with external services which require user authorization via the OAuth framework.
OAuth Support in Riiiver
Among the four protocol flows outlined in RFC6749("The OAuth 2.0 Authorization Framework"), Riiiver currently supports only the authorization code flow. Furthermore, OAuth 1.0 is not supported.
Support Status for OAuth protocol flows:
Name of Flow | Supported? | Notes |
---|---|---|
Authorization Code | ○ | |
Implicit | - | |
Resource Owner Password Credentials | - | |
Client Credentials | - |
When requesting a token endpoint from the authorization server of an external service you want to use, client_secret_post
or client_secret_basic
methods should be used. [Reference]
In addition, although the Google API is not fully compliant with the RFC framework, access_token request is supported.
Request Method | Supported? | postType specifications | Notes |
---|---|---|---|
client_secret_post | ○ | form_urlencoded | |
client_secret_basic | ○ | base64 | |
client_secret_jwt | - | ||
private_key_jwt | - | ||
tls_client_auth | - | ||
self_signed_tls_client_auth | - | ||
Other (Google API) | ○ | json |
Creating Pieces that Connect to Google Calendar
To create a Piece that works with an external service requiring OAuth authorization, a few items must be prepared. Below, using "Google Calendar" as our example service, we will explain (in order) the files necessary to include in a Piece connected to an external service requiring an OAuth authorization flow.
- OAuth JSON file(oauth.json)
- PieceJSON file(piece.json)
- node_modules (request, request-promise installed)
- PieceCore js file(PieceCore.js)
OAuth JSON File
First, let's look at the contents of the oauth.json file.
{ "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" } }
This file describes the information required to make a request to the authorization server of the external service.
- When using OAuth2.0 as your
" authType "
," oauth2 "
should be specified. As mentioned previously, the only" authType "
currently supported by Riiiver is" oauth2 "
(Reference Page: PieceJSONKeys). - In
" postType "
, specify the format by which data will be transmitted to the authorization server. Check the documentation of the external service to be used, and specify an appropriate value from this chart.
In our example, we are using a Google API, so" json "
is specified as the" postType "
. - In the
"oauth2"
section、describe the information needed for the authorization flow:
Parameters specified within oauth2
keys | description |
---|---|
authorizedUrl | Authorization URL specified by external service |
tokenUrl | URL for token distribution provided by external service |
redirectUri | Port for receiving authorization server response (no change required) |
clientId | ID issued by external service |
clientSecret | Password issued by external service |
scope | Scope of permission granted |
state | May or may not be required, depending on service used. Check documentation of the external service. |
Upon uploading a PieceJSON with PieceBuilder, you will also see the option to "Choose an OAuth file" before your final submission (this option appears in the same area where you can "Choose ServiceProxyCore," "Choose an icon file," and "Choose an illustration file). Please be sure to upload the OAuth file here.
See "「Register Your Piece」" for detailed instructions on how to upload Pieces.
About the PieceJSON File
Now we'll explain the OAuth coding within the PieceJSON file.
Add " authType "
underneath " ServiceProxy "
as demonstrated below and then specify " oauth2 "
. Essentially, this is the only description required to integrate OAuth into the JSON. The remainder of what was described in the previous section is processed on the server side, so there is no need to specify the file name of OAuth JSON within the PieceJSON.
"serviceProxy": { "service": "googleCalendarWatch", "authType": [ "oauth2" ] }
See how it fits into the larger PieceJSON file here:
{ "title": { "ja": "Google カレンダー", "en": "Google calendar" }, "version": "0.0.1", "sdkVersion": "0.0.1", ・・・(sdk details)・・・, "blockType": "service", "executor": "RBCCommonWebServiceExecutor", "serviceProxy": { "service": "googleCalendarWatch", "authType": [ "oauth2" ] }, "output" : { "type" : "object", "properties": { ・・・(properties details)・・・ } } }, "categoryIds": [ "cat_0007" ] }
Preparing node_modules
request-promise: Installation
request-promise is a package that can easily obtain information via HTTP communication. Since it relies on the request
package, you will need to have therequest
package installed.
If you have already made an S Piece using API, you can simply execute the command seen on the second line below because the request
package should already be installed.
npm install request npm install request-promise
request-promise: How to Use
When you want to retrieve Google html, set the following configuration to describe the processing both when .then
succeeds and when.catch
fails.
var rp = require('request-promise'); rp('http://www.google.com') .then(function (htmlString) { // Process html... }) .catch(function (err) { // Crawling failed... });
For basic information about how to use request-promise, please refer to this link.
About PieceCore.js
Finally, let's talk about PieceCore.js. Below is a sample code that can retrieve up to three events from Google Calendar and sort them by start time.
const requestPromise = require('request-promise'); exports.handler = async (event) => { if (event.call === 'lambda') { console.log('CALLED:LAMBDA'); /* Fill in when using an external module. ex : if ( xxxx !== null ){} // xxxx : generated instance */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 format "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, // '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)}`); //When the schedule stretches beyond midnight, that result will be 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}); } } }); // Sort schedule by start time plans.sort(function(a, b) { // Generate date format for comparison. (We just want to compare by time, so year/month/day alone is suitable) 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: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getTimeInfo (date) { var index = date.indexOf('T') + 1; // extract and return HH:mm return date.substring( index, index + 5 ); } // date: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getDateInfo (date) { var index = date.indexOf('T'); // extract and return yyyy-mm-dd return date.substring( 0, index ); } // date: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getDayInfo (date) { var index = date.indexOf('T'); // extract and return yyyy-mm-dd return date.substring( index -2, index ); } // date: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getTimeZoneInfo (date) { // extract and return ZZZ return date.substring( date.length - 5, date.length ); }
Sample Code Explanation
In the above sample code, the API is called using the access_token
acquired by OAuth authorization, and the output from the response is sent to the next Piece.
Below, we slice apart the structure of the sample code and explain it in order of operation.
1. Get the Block/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; … /** * The processing described in the following section continues. */ }).catch(error => { /** * Processing for when the Block/Piece ID cannot be obtained per usual */ responseData = { status: error.statusCode, body: { message: error.message } }; return responseData; }).finally(() => { console.log(`getGoogleCalendarEvents response data: ${ JSON.stringify(responseData) }`); }); };
2. Get the user's Auth information stored on the Riiiver server. 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; } … /** * The processing described in the following section continues. */}).catch(error => { /** * Processing for when the Auth cannot be obtained per usual, such as when it is not stored in the server. */ responseData = { status: error.statusCode, body: { message: error.message } }; return responseData; });
3. Get an access_token
. If the access_token
is only active for a limited time (which will depend on the rules set by the specific external Auth service), you can also acquire expires_in
and refresh_token
information from the response.
If cases where the access_token
has expired, you can use the Riiiver API to refresh a new 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); /** * Determine if the access_token has expired * expires_in (token expiration date) value is smaller than current value * In other words, if the active period has expired, try to get the access_token again. */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)Process with the newly acquired access_token * When actually making your own Piece, change the process accordingly here and in (★2) below. */ return getEvents(access_token, event, date).then(response => { responseData = response; return responseData; }); }); } else { const access_token = body.oauth2.access_token; /** * (★2)Process when access_token has not expired */ return getEvents(access_token, event, date).then(response => { responseData = response; return responseData; }); } …
4. After obtaining up to three events from Google Calendar, sort them by start time and generate your output.
/** * Processing called on in (★1, 2) */const getEvents = async (access_token, event, today) => { // date format "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)}`); //When the schedule stretches beyond midnight, the result will be 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 }); } } }); // Sort scheduled events by start time plans.sort(function (a, b) { // Generate date format for comparison. (We just want to compare by time, so year/month/day alone is suitable) 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: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getTimeInfo(date) { var index = date.indexOf('T') + 1; // extract and return HH:mm return date.substring(index, index + 5); } // date: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getDateInfo(date) { var index = date.indexOf('T'); // extract and return yyyy-mm-dd return date.substring(0, index); } // date: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getDayInfo(date) { var index = date.indexOf('T'); // extract and return yyyy-mm-dd return date.substring(index - 2, index); } // date: Date string (yyyy-mm-ddTHH:mm:ssZZZ) function getTimeZoneInfo(date) { // extract and return ZZZ return date.substring(date.length - 5, date.length); }
Appendix:Creating Pieces that Write Data into Google Sheets
To this point, we have described how to make a Piece which can get data from Google Calendar. Instead of getting data, let's now reverse our example and think of a Piece that can write data into a Google spreadsheet.
In fact, the information described in the above Calendar Sample Code1〜3 can pretty much be used as is. You're just going to rewrite the process by which you're calling the API using an access_token, switching the (★ 1, 2) information in sections 3 and 4.
Now take a look at the Piece code below, where the specific spreadsheet ID (sheetId
) and contents (input
) would be preference information entered by the end-user:
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 } }); }); ); };
Note that you're also going to need to change the scope in the oauth.json
to allow write permission to the spreadsheet, as well as enable the Google Sheets API:
"scope": "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/spreadsheets",