I am building a Woocommerce API integration and am working on the authentication functionality. The authorization endpoint requires a return_url and a callback_url.
I am under the impression that I can use the StateTokenBuilder, similar to how the OAuth2 library functions but am running into issues trying to implement based on the Apps Script docs.
Of course, the UrlFetchApp to the Woocommerce endpoint works fine, but when trying to authorize the app at the endpoint, I get the following error:
Error: An error occurred in the request and at the time were unable to
send the consumer data.
After subsequent tests with Postman, the callback URL, and return URLs evaluate to the Google Drive splash page and do not execute the function in the script. I am at a loss for how to access the callback function from another source, as it appears that it is only accessible via browser, when logged in.
Below are my functions:
let endpoint = {
protocol: 'https',
base: '{URL}.com'
}
let params = {}
API.Auth.Authorize = options => {
endpoint = { ...endpoint, ...{ section: 'wc-auth/v1/authorize' } };
options = { ...params, ...options };
let method = 'GET';
const query =
method === 'GET' && typeof params === 'object'
? Object.keys(options)
.filter(param => param !== 'payload')
.map(param => `${param}=${encodeURIComponent(options[param])}`)
.join('&')
: options;
const url = `${endpoint.protocol}://${endpoint.base}/${endpoint.section}${typeof query === 'string' && query.length > 0 ? `?${query}` : ''}`;
let opts = {
method: method,
muteHttpExceptions: true
};
return url;
};
const authorize = () => {
const params = {
app_name: 'App Name',
scope: 'read_write',
user_id: 'abc123',
return_url: 'https://script.google.com/macros/s/{SCRIPT_ID}/exec',
callback_url: generateCallbackURL('handleCallback')
};
const result = API.Auth.Authorize(params);
console.log(result);
}
generateCallbackURL = fn => {
const script_url = `https://script.google.com/macros/d/${ScriptApp.getScriptId()}`;
const url_suffix = '/usercallback?state=';
const state_token = ScriptApp.newStateToken().withMethod(fn).withTimeout(120).createToken();
const callback_url = `${script_url}${url_suffix}${state_token}`;
return callback_url;
}
const handleCallback = e => {
return HtmlService.createHtmlOutput(JSON.stringify(e));
}
I have also tried replacing the handleCallback function with a doPost function to determine if that would solve the issue. It did not have an effect.
Related
I would want to get the ID token to be consumed in google appscript web app (which needs my account) in order to access the API.
My end goal is to know how can I achieve getGoogleIDTokenToInvokeAppScriptEndPoint (I have added in my sample code).
What exactly needs to be done to programatically access ID Token to invoke AppScript APIs from external service.
I am not sure how to generate the same programatically. Do I need to create a service account on my behalf?
Lets say I created a google account as abc#gmail.com, so I need to access the service account programatically for this user only and not some other service account. I then want to get the ID token and invoke my appscript services using the same.
Here's what I have already done:
Created my doPost request and deployed it like this:
Now the URL that I get can only be invoked if it has a bearer ${token}, where token is my ID token.
My question is how to possibly get this ID token programatically from an external service? I am using Node Application and I would like something like:
// External Script from where I am invoking AppScript API
const token = getGoogleIDTokenToInvokeAppScriptEndPoint();
// Now Since I got my ID Token, I could use the same
// to invoke my appScript app:
fetch(appScriptEndpoint,
{
method:"POST",
headers: {
Authorization: `Bearer ${token}` // Token I got from first step
}
}).then(res => res.json().then(data => data))
Here's how my appscript looks like (though not much relevant here, but still adding for detailing):
// AppScript Script
const doPost = (request = {}) => {
const { _, postData: { contents, type } = {} } = request;
let query = {};
if (type === 'application/json') {
query = JSON.parse(contents);
} else if (type === 'application/x-www-form-urlencoded') {
contents
.split('&')
.map((input) => input.split('='))
.forEach(([key, value]) => {
query[decodeURIComponent(key)] = decodeURIComponent(value);
});
} else if (type === 'text/plain') {
try {
query = JSON.parse(contents);
} catch (e) {
return ContentService.createTextOutput(
JSON.stringify({
error: true,
statusCode: 400,
msg: 'Unable to Parse JSON',
type: type,
requestBody: contents,
payload: {}
})
).setMimeType(ContentService.MimeType.JSON);
}
} else
return ContentService.createTextOutput(
JSON.stringify({
error: true,
statusCode: 400,
msg: 'Unknown Request Type',
type: type,
requestBody: contents,
payload: {}
})
).setMimeType(ContentService.MimeType.JSON);
const apiKey = query.apiKey;
const isAuthenticated = authenticate({apiKey});
if(!isAuthenticated) return ContentService.createTextOutput(
JSON.stringify({
error: true,
statusCode: 401,
msg: 'User not authorized to make the Request.',
type: type,
requestBody: contents,
payload: {}
})
).setMimeType(ContentService.MimeType.JSON);
else {
const {opType,opProps} = query;
const result = requestHandler({opType, opProps});
return ContentService.createTextOutput(
JSON.stringify({
error: false,
statusCode: 200,
msg: 'Connection Established',
type: type,
requestBody: contents,
payload: result
})
).setMimeType(ContentService.MimeType.JSON);
}
};
Please note that I already know how to invoke it by keeping my web app open however, I want to invoke it when authentication is required as shown in the above configuration.
I have read MANY docs/blogs/SO articles on using the Firebase Emulator Suite trying a simple pubsub setup, but can't seem to get the Emulator to receive messages.
I have 2 functions in my functions/index.js:
const functions = require('firebase-functions');
const PROJECT_ID = 'my-example-pubsub-project';
const TOPIC_NAME = 'MY_TEST_TOPIC';
// receive messages to topic
export default functions.pubsub
.topic(TOPIC_NAME)
.onPublish((message, context) => {
console.log(`got new message!!! ${JSON.stringify(message, null, 2)}`);
return true;
});
// publish message to topic
export default functions.https.onRequest(async (req, res) => {
const { v1 } = require('#google-cloud/pubsub');
const publisherClient = new v1.PublisherClient({
projectId: process.env.GCLOUD_PROJECT,
});
const formattedTopic = publisherClient.projectTopicPath(PROJECT_ID, TOPIC_NAME);
const data = JSON.stringify({ hello: 'world!' });
// Publishes the message as JSON object
const dataBuffer = Buffer.from(data);
const messagesElement = {
data: dataBuffer,
};
const messages = [messagesElement];
// Build the request
const request = {
topic: formattedTopic,
messages: messages,
};
return publisherClient
.publish(request)
.then(([responses]) => {
console.log(`published(${responses.messageIds}) `);
res.send(200);
})
.catch((ex) => {
console.error(`ERROR: ${ex.message}`);
res.send(555);
throw ex; // be sure to fail the function
});
});
When I run firebase emulators:start --only functions,firestore,pubsub and then run the HTTP method with wget -Sv -Ooutput.txt --method=GET http://localhost:5001/my-example-pubsub-project/us-central1/httpTestPublish, the HTTP function runs and I see its console output, but I can't seem to ever get the .onPublish() to run.
I notice that if I mess around with the values for v1.PublisherClient({projectId: PROJECT_ID}), then I will get a message showing up in the GCP cloud instance of the Subscription...but that's exactly what I don't want happening :)
I'm trying to access the contentDocument of a cross origin iframe using Runtime.evaluate. As far as I understand the docs this should be possible by creating an executionContext with universal access using Page.createIsolatedWorld + grantUniveralAccess: true [1] and passing the returned executionContextId to Runtime.evaluate as contextId.
Any ideas?
Given a chromium process started with chromium-browser --user-data-dir=/tmp/headless --remote-debugging-port=9000 [2].
// See [3] for full code
const frameId = /* frameId of our page with origin localhost:9000 */
function execute(command, args) { /* ... send and receive on websocket */ }
const {executionContextId} = await execute("Page.createIsolatedWorld", {
frameId: frameId,
grantUniveralAccess: true // NOT grantUniversalAccess. Typo in devtools protocol itself [4].
})
// fails with:
// Access to fetch at 'http://example.com/' from origin 'http://localhost:9000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
await execute("Runtime.evaluate", {
awaitPromise: true,
expression: `fetch("http://example.com").then(r => r.text())`,
contextId: executionContextId
})
// fails with:
// Uncaught DOMException: Blocked a frame with origin "http://localhost:9000" from accessing a cross-origin frame.
execute("Runtime.evaluate", {
awaitPromise: true,
expression: `
new Promise((resolve, reject) => {
const iframe = document.createElement("iframe");
iframe.src = "http://example.com"
iframe.onload = () => resolve(iframe)
iframe.onerror = reject;
document.body.append(iframe)
}).then(iframe => iframe.contentWindow.document)`,
contextId: executionContextId
})
[1] I would have expected universal access to allow me to acess cross origin resources the same way the --disable-web-security flag does - which internally grants universal access
if (!frame_->GetSettings()->GetWebSecurityEnabled()) {
// Web security is turned off. We should let this document access
// every other document. This is used primary by testing harnesses for
// web sites.
origin->GrantUniversalAccess();
[2] Running head-full for easier debugging (e.g. seeing the full cors error only printed to the console) - running with --headless doesn't work either.
[3]
const targets = await fetch("http://localhost:9000/json").then(r => r.json());
const tab = targets.filter(t => t.type === "page")[0];
let counter = 0, commands = {};
const w = new WebSocket(tab.webSocketDebuggerUrl);
await new Promise(resolve => { w.onopen = resolve; })
w.onmessage = event => {
const json = JSON.parse(event.data)
if (commands[json.id]) commands[json.id](json);
else console.log(json); // event
}
function execute(method, params) {
return new Promise((resolve, reject) => {
const id = counter++;
commands[id] = ({result, error}) => {
console.log(method, params, result, error)
if (error) reject(error);
else resolve(result);
// delete commands[id];
};
w.send(JSON.stringify({method, id, params}));
});
}
window.execute = execute;
window.frameId = tab.id;
[4] The correct parameter name is grantUniveralAccess (no s in univeral). Easily validated by passing a value with an incorrect type (expects a bool)
// fails with:
// Failed to deserialize params.grantUniveralAccess - BINDINGS: bool value expected at position 69
await execute("Page.createIsolatedWorld", {frameId, grantUniveralAccess: "true"})
I am calling a google app script from my firebase cloud function as shown below. I am able to call the script successfully and create a google form, but am not able to
a. send a parameter/ data from the cloud function to the google app script when calling the script.run
b. get the right data (url) for the created form back from the google app script in the response to the cloud function.
I am new to app script. Please help me understand what I am doing wrong.
My cloud function code:
import * as functions from "firebase-functions";
const fs = require("fs");
const { google } = require("googleapis");
const googleAuth = require("google-auth-library");
const script = google.script("v1");
const scriptId = "MY_SCRIPT_ID";
// calling the cloud function from my javascript app
export const gpublish = functions.https.onCall((data: any, response: any) => {
const test = data.test;
return new Promise((resolve, reject) => {
// Authenticating with google app script
fs.readFile("gapi_credentials.json", (err: any, content: string) => {
const credentials = JSON.parse(content);
const { client_secret, client_id, redirect_uris } = credentials.web;
const functionsOauth2Client = new googleAuth.OAuth2Client(client_id, client_secret,redirect_uris);
functionsOauth2Client.setCredentials({refresh_token: credentials.refresh_token});
// call the google app script
return runScript(functionsOauth2Client, scriptId, test.testName)
.then((scriptData: any) => {
console.log("returned script response is " + JSON.stringify(scriptData));
})
.catch((err4) => {
console.log("There is some problem with the script running ");
return 'ERROR_RESPONSE';
});
}); }); });
function runScript(auth: any, scriptid: string, testName: string) {
return new Promise(function (resolve, reject) {
script.scripts.run(
{
auth: auth,
scriptId: scriptid,
resource: {
function: "doPost",
parameters: testName,
},
},
function (err3: any, respons: any) {
if (err3) {
console.log("API returned an error: " + err3);
reject(err3);
}
else {
console.log(" the script is run and response is " + JSON.stringify(respons));
resolve(respons);
}
});
});
}
My google app script is below:
function doPost(e) {
var postJSON = e.parameter; // e is undefined eventhough data is being passed
console.log("postJSON is: "+ JSON.stringify(postJSON));
doGet();
}
function doGet(e) {
// create & name Form
var item = "Sample Form_SMT";
var form = FormApp.create(item)
.setTitle(item);
// single line text field
... some code to create the google form
// the form url is correctly logged.
var url = form.getEditUrl();
console.log("url of the form is " + URL);
// id of the form is correctly logged
var formId = form.getId();
console.log("the id of the form is " + formId);
const result = {'url': url};
var JSONString = JSON.stringify(result);
var JSONOutput = ContentService.createTextOutput(JSONString);
JSONOutput.setMimeType(ContentService.MimeType.JSON);
return JSONOutput; // this output is NOT being returned to the cloud function
}
On the google app script log Iam getting this error:
ReferenceError: e is not defined
On the cloud function log the response returned says status: 200, done: true, but the JSON output above is not being returned.
You might want to try this sort of approach.
function doPost(e) {
var postJSON = e.parameter; // e is undefined eventhough data is being passed
console.log("postJSON is: "+ JSON.stringify(postJSON));
return func1(e);
}
function doGet(e) {
return func1(e)
}
function func1(e) {
// create & name Form
var item = "Sample Form_SMT";
var form = FormApp.create(item)
.setTitle(item);
// single line text field
... some code to create the google form
// the form url is correctly logged.
var url = form.getEditUrl();
console.log("url of the form is " + URL);
// id of the form is correctly logged
var formId = form.getId();
console.log("the id of the form is " + formId);
const result = {'url': url};
var JSONString = JSON.stringify(result);
var JSONOutput = ContentService.createTextOutput(JSONString);
JSONOutput.setMimeType(ContentService.MimeType.JSON);
return JSONOutput; // this output is NOT being returned to the cloud function
}
The answer above solved the problem of sending data to the app script.
In addition, I did the following to make sure that the response data was correctly returned from app script back to the cloud function. I changed what I was calling from doPost to doGet as below:
function runScript(auth: any, scriptid: string, testName: string) {
return new Promise(function (resolve, reject) {
script.scripts.run(
{
auth: auth,
scriptId: scripted,
resource: {
function: "doGet",
parameters: testName,
},
},
function (err3: any, respons: any) {
if (err3) {
console.log("API returned an error: " + err3);
reject(err3);
}
else {
console.log(" the script is run and response is " + JSON.stringify(respons));
resolve(response);
}
});
});
}
In google app script, I wrote doGet as below:
function doGet(e) {
const returnResult = generateTest(e);
return JSON.stringify(returnResult)
}
I am trying to change the required engine version of an AppPackage that I have posted using v2 of the Design Automation API.
I've tried using Postman and the Forge Node Client. I'm using the Forge documentation as a reference.
https://forge.autodesk.com/en/docs/design-automation/v2/reference/http/AppPackages(':id')-PATCH/
My credentials are correct and I have a valid token, but for some reason I keep getting a 404 Not Found status and an error that says "AppPackage with the name MyPlugin doesn't belong to you. You cannot operate on AppPackage you do not own." Also, I get the same message when I try to delete or update the AppPackage.
That's really weird because I definitely own this AppPackage. I uploaded it with these same credentials and I can view it by doing a GET request to view all of my AppPackages. Furthermore, the name of the AppPackage is correct and I specified the right scope (code:all) when I authenticated.
Why does Design Automation think this AppPackage doesn't belong to me and why can't I patch, update, or delete it?
UPDATE 3/28/2019: Setting the resource value still results in the same error
UPDATE 4/2/2019: Getting a fresh upload URL doesn't work either. I get an internal server error saying "Object reference not set to an instance of an object."
const ForgeSDK = require('forge-apis');
const oAuth2TwoLegged = new ForgeSDK.AuthClientTwoLegged(FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, SCOPES);
const appPackageApi = new ForgeSDK.AppPackagesApi();
const getToken = () => {
return oAuth2TwoLegged.authenticate();
};
const getUploadURL = () => {
return appPackageApi.getUploadUrl(oAuth2TwoLegged, oAuth2TwoLegged.getCredentials());
};
const patchPackage = (id, url) => {
const appPack = {
Resource: url,
RequiredEngineVersion: APP_PACKAGE_REQUIRED_ENGINE
};
return appPackageApi.patchAppPackage(id, appPack, oAuth2TwoLegged, oAuth2TwoLegged.getCredentials());
};
(async () => {
try {
const token = await getToken();
const url = await getUploadURL();
const patchPackRes = await patchPackage(APP_PACKAGE_ID, url);
if (patchPackRes.statusCode == 201)
console.log('Patch package succeeded!');
else
console.log('Patch package failed!' + patchPackRes.statusCode);
} catch (ex) {
console.log('Exception :(');
console.log(ex);
}
})();
When calling PATCH the "Resource" property must be set. It can be set to the same URL as the one you receive from GET but it must be present and valid.
This should work:
const ForgeSDK = require('forge-apis');
const oAuth2TwoLegged = new ForgeSDK.AuthClientTwoLegged(FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, SCOPES);
const appPackageApi = new ForgeSDK.AppPackagesApi();
const getToken = () => {
return oAuth2TwoLegged.authenticate();
};
const getUploadURL = async (id) => {
const app = await appPackageApi.getAppPackage(id, oAuth2TwoLegged, oAuth2TwoLegged.getCredentials());
return app.body.Resource;
};
const patchPackage = (id, url) => {
const appPack = {
Resource: url,
RequiredEngineVersion: APP_PACKAGE_REQUIRED_ENGINE
};
return appPackageApi.patchAppPackage(id, appPack, oAuth2TwoLegged, oAuth2TwoLegged.getCredentials());
};
(async () => {
try {
const token = await getToken();
const url = await getUploadURL(APP_PACKAGE_ID);
const patchPackRes = await patchPackage(APP_PACKAGE_ID, url);
if (patchPackRes.statusCode == 201)
console.log('Patch package succeeded!');
else
console.log('Patch package failed!' + patchPackRes.statusCode);
} catch (ex) {
console.log('Exception :(');
console.log(ex);
}
})();